In this article, I will discuss how to work with the Apollo Client, the new GraphQL client library that is used in the Faust.js framework.
The New Faust.js
Faust.js is the front-end JavaScript framework built on top of Next.js, created to make developing headless WordPress sites easier. The idea of this new version is to make a parity between the WordPress world that a developer might be used to but replicate it as much as possible in the JavaScript front-end world.
The Apollo Client
Faust uses Apollo for its GraphQL client. The Apollo client is a data-fetching library for JavaScript that enables you to manage both local and remote data with GraphQL. It has features you can use for fetching, modifying app data, and caching. We are going to focus on how to use it in Faust.
Usage in Faust.js
Within Faust, you can use the client in your components and pages. Take this example here from the Faust docs:
import { gql, useQuery } from '@apollo/client';
export default function Page(props) {
const { data } = useQuery(Page.query);
const { title, description } = data.generalSettings;
return(
<h1>{title}</h1>
<p>{description}</p>
)
}
Page.query = gql`
query {
generalSettings {
title
description
}
}
`
Let’s go over what is happening in this file. At the top of the file, graphql
and the useQuery
hook from Apollo is being imported into the file. Then we have a default function called Page
that will render the title
and description
of the general settings from your WPGraphQL API. After passing the props into the Page
function, we create a variable that contains the data
object that is the destructured response from the useQuery
hook provided by Apollo. This then takes Page.query
as a parameter.
At the bottom of the file is our actual GraphQL query which Page.query
is coming from as a parameter. We are calling in the constant variable with the useQuery
hook.
Back above the return statement, we have an object that contains the destructured fields queried which are the title
and description
from the data in WordPress generalSettings
.
It co-locates the GraphQL query within the same file. You can also import and use queries from a file.
The benefit of Faust is that you don’t have to create the client object in your codebase. It is automatically created when you first import the @faustwp-core
package. Instead, you can customize it by using a plugin filter.
Custom plugin filter 🔌: Pagination example
For this article, let’s create a custom plugin filter to use cursor-based pagination in Faust.js with Apollo. If you need a deeper understanding of pagination, please check out this article I wrote on Pagination in Headless WP.
The first thing we need to do is go into the components
folder at the root of the Faust project. In the components folder, create a file called LoadMorePost.js
and paste this code block in the file:
import { useQuery, gql } from "@apollo/client";
import Link from "next/link";
const GET_POSTS = gql`
query getPosts($first: Int!, $after: String) {
posts(first: $first, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
databaseId
title
slug
}
}
}
}
`;
const BATCH_SIZE = 5;
export default function LoadMorePost() {
const { data, loading, error, fetchMore } = useQuery(GET_POSTS, {
variables: { first: BATCH_SIZE, after: null },
});
console.log(data);
if (error) {
return <p>Sorry, an error happened. Reload Please</p>;
}
if (!data && loading) {
return <p>Loading...</p>;
}
if (!data?.posts.edges.length) {
return <p>no posts have been published</p>;
}
const posts = data.posts.edges.map((edge) => edge.node);
const haveMorePosts = Boolean(data?.posts?.pageInfo?.hasNextPage);
return (
<>
<ul style={{ padding: "0" }}>
{posts.map((post) => {
const { databaseId, title, slug } = post;
return (
<li
key={databaseId}
style={{
border: "2px solid #ededed",
borderRadius: "10px",
padding: "2rem",
listStyle: "none",
marginBottom: "1rem",
}}
>
<Link href={`${slug}`}>{title}</Link>
</li>
);
})}
</ul>
{haveMorePosts ? (
<form
method="post"
onSubmit={(event) => {
event.preventDefault();
fetchMore({ variables: { after: data.posts.pageInfo.endCursor } });
}}
>
<button type="submit" disabled={loading}>
{loading ? "Loading..." : "Load more"}
</button>
</form>
) : (
<p>✅ All posts loaded.</p>
)}
</>
);
}
Let’s break this file down into chunks. At the top of the file, I am importing the useQuery
hook and gql
provided by the Apollo client that I am using as well as next/link
from Next.js. We will need these imports in this file.
The next thing you see is the query we created with cursor-based pagination.
const GET_POSTS = gql`
query getPosts($first: Int!, $after: String) {
posts(first: $first, after: $after) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
id
databaseId
title
slug
}
}
}
}
`;
const BATCH_SIZE = 5;
After that, I have a default components function called LoadMorePost
. In this function, I am utilizing the useQuery
hook in Apollo to pass in my query called GET_POSTS
from the top of the file.
Next, I have variables that I pass in, which is the batch size
I defined to be 5
, and after null which tells the query to start from the beginning of the batch. This function gets fired off each time the user clicks the “load more”
button.
export default function LoadMorePost() {
const { data, loading, error, fetchMore } = useQuery(GET_POSTS, {
variables: { first: BATCH_SIZE, after: null },
});
console.log(data);
if (error) {
return <p>Sorry, an error happened. Reload Please</p>;
}
if (!data && loading) {
return <p>Loading...</p>;
}
if (!data?.posts.edges.length) {
return <p>no posts have been published</p>;
}
There are 2 variables that get set next. The first variable is posts
which is taking the data that Apollo gives us back and drilling down into it with the posts
and their nested data. The second variable is haveMorePosts
which checks if we have more posts to load but if there are no more posts
we will have to execute something else.
const posts = data.posts.edges.map((edge) => edge.node);
const haveMorePosts = Boolean(data?.posts?.pageInfo?.hasNextPage);
So now we can display our posts with a return statement with some data drilling within the levels of nesting that comes from the query.
Focusing now on the return statement, we have a <ul>
tag. Within that tag, we are mapping over posts and returning a single post with a databaseId
, title
, and its slug
. For each of those, we are displaying a list item with a <li>
tag. This list item will have a title that has a link to the actual individual blog post’s page.
return (
<>
<ul style={{ padding: "0" }}>
{posts.map((post) => {
const { databaseId, title, slug } = post;
return (
<li
key={databaseId}
style={{
border: "2px solid #ededed",
borderRadius: "10px",
padding: "2rem",
listStyle: "none",
marginBottom: "1rem",
}}
>
<Link href={`${slug}`}>{title}</Link>
</li>
);
})}
</ul>
Lastly, we have to add a “load more”
button. This button when clicked will load the next batch of posts from the cursor’s point. In order to do this, we take our haveMorePosts
boolean and if we do have more, we will display a form with a button inside of it. When that button is clicked, we have a onSubmit
handler that calls the fetchMorefunction
in Apollo and passes in the variable called after that grabs the current end cursor, which is the unique ID that represents the last post in the data set to grab the next 5 after that end cursor.
{haveMorePosts ? (
<form
method="post"
onSubmit={(event) => {
event.preventDefault();
fetchMore({ variables: { after: data.posts.pageInfo.endCursor } });
}}
>
<button type="submit" disabled={loading}>
{loading ? "Loading..." : "Load more"}
</button>
</form>
) : (
<p>✅ All posts loaded.</p>
)}
</>
);
}
Now that we have created our component in Faust for a paginated page to load posts in batches of 5, the next step is to create a page to test this out. Navigate to the pages directory in the root of the project and create a file called pagination.js
.
In that file, copy and paste this code block in:
import Head from "next/head";
import LoadMorePost from "../components/LoadMorePost";
export default function LoadMore() {
return (
<>
<Head>
<title>Load More</title>
</Head>
<main>
<h1>Load More Example</h1>
<LoadMorePost />
</main>
</>
);
}
In this file, we are importing the component into this file and exporting it as a default function, returning it to render on the page.
Custom Plugin Creation in Faust for the client object
The Apollo client can implement relay-style pagination with the relay spec using merge and read functions, which means all the details of connections
, edges
and pageInfo
can be abstracted away, into a single, reusable helper function. WPGraphQL follows the relay spec as well.
What we need to do is create a plugin for relay-style pagination in order for Faust to utilize that function from Apollo.
In order to create a Faust plugin, we are going to use its apply
method which is a JavaScript class. The apply
method has a parameter called hooks
which is passed from @wordpress/hooks.
The first step is to go to the root of the project and create a folder called plugins
. In this plugin folder, create a file called RelayStylePagination.js
. Copy and paste this code block into that file:
import { relayStylePagination } from "@apollo/client/utilities";
export class RelayStylePagination {
apply(hooks) {
const { addFilter } = hooks;
addFilter("apolloClientInMemoryCacheOptions", "faust", (options) => {
return {
...options,
typePolicies: {
...options.typePolicies,
RootQuery: {
...options.typePolicies.RootQuery,
fields: {
posts: relayStylePagination(),
},
},
ContentType: {
fields: {
contentNodes: relayStylePagination(),
},
},
},
};
});
}
}
At the top of the file, we import the relayStylePagination
function from Apollo’s utility library. Following that, we create a class component
which is the basic syntax used in a Faust plugin.
Next, we have an apply
method with the hooks
parameter which is an object that gives you a function called addFilter
. The addFilter
function allows us to modify the Apollo Client’s InMemoryCacheOptions
.
In the next few lines of code, we are taking the addFilter
hook and calling the memory cache
function options
. The options
are coming from the Apollo Client Cache configuration. These options
allow for configuring the cache’s behavior to suit our use case. In this article, we are defining the configuration for pagination. The faust
namespace follows that.
Next, we spread through our options
which is a callback function that can return the inMem cache options
as they are. We can also merge in our own with typePolicies
that we define along with other specific queries we need to merge in the future. The typePolicies
is a class that defines a policy of your type in Apollo. Here, we are adding it to the RootQuery
option:
addFilter("apolloClientInMemoryCacheOptions", "faust", (options) => {
return {
...options,
typePolicies: {
...options.typePolicies,
RootQuery: {
...options.typePolicies.RootQuery
The last few lines of code are where we are defining our fields of the posts
type. Once those are defined, we can now use the relay spec used by WPGraphQL to tell Apollo and its spec in our pagination method to go ahead and append the previous and next list together using cursor-based pagination by calling the relayStylePagination
function provided. Another thing to note is I also added ContentType
to expose the contentNodes
fields in case of using something like search functionality.
fields: {
posts: relayStylePagination(),
},
},
ContentType: {
fields: {
contentNodes: relayStylePagination(),
},
},
},
};
});
}
}
Lastly, we need to go to our faust.config.js
file to imbed it into the experimentalPlugins
key as a value like so:
import { setConfig } from "@faustwp/core";
import templates from "./wp-templates";
import possibleTypes from "./possibleTypes.json";
import { RelayStylePagination } from "./plugins/RelayStylePagination";
/**
* @type {import('@faustwp/core').FaustConfig}
**/
export default setConfig({
templates,
experimentalPlugins: [new RelayStylePagination()],
possibleTypes,
});
Generating possible types in Apollo
Before you run the dev server, make sure you type the command npm run generate
since we updated the schema. The Apollo client requires that we provide a possibleTypes
object that maps over our updated schema. The Faust framework provides the command script for you.
Stoked! Once you have added it to the config file in the plugins, this will allow you to have the pagination you expect when the user clicks your load more button and expects the next batch of posts!!
Previous and Next Post Link extension of WPGraphQL
The last piece I want to cover is using the WP Template to your advantage to create components and where those GraphQL queries will live and co-locate.
Out of the box, WPGraphQL does not have the ability to expose and query for previous
and next
post. The pagination fields plugin will allow us to modify the GraphQL schema and accomplish this.
Once this plugin is downloaded into your WP admin, you can now query WPGraphQL for the links. The query I made looks like this which you can run in GraphiQL IDE with the slug
as the query variable:
query getPost($slug: ID!) {
post(id: $slug, idType: SLUG) {
databaseId
title
content
slug
previousPost {
title
slug
}
nextPost {
title
slug
}
}
}
I am querying for a single post via its slug as the variable. At the bottom of the query is where I am able to also query for the previous and next posts with the single post query as the starting point.
Finalizing Previous and Next posts in Faust.js
Back in my faust.js app, navigate to the components directory at the root of the project. From there, go to the Footer
folder and create a file called PaginatedFooter.js
and copy this block and paste it in:
import Link from "next/link";
export default function PaginatedFooter({ post }) {
const { previousPost, nextPost } = post;
return (
<>
<footer style={{ display: "flex", textAlign: "center" }}>
{previousPost ? (
<div
style={{
border: "2px solid #ddd",
padding: "1rem",
}}
>
<Link href={`${previousPost.slug}`}>
<a>👈 {previousPost.title}</a>
</Link>
</div>
) : null}
{nextPost ? (
<div
style={{
border: "2px solid #ddd",
padding: "1rem",
marginLeft: "1rem",
}}
>
<Link href={`${nextPost.slug}`}>
<a>{nextPost.title} 👉</a>
</Link>
</div>
) : null}
</footer>
</>
);
}
At the top of the file, I am importing Link
from next/link
using Next.js’s client-side navigation to link pages.
Then I have a default function called PaginatedFooter
that is accepting the post
data as its props. The following is a constant variable that destructures the post
props of previousPost
and nextPost
which we now have exposed and are querying from WPGraphQL.
export default function PaginatedFooter({ post }) {
const { previousPost, nextPost } = post;
Lastly, I have a return statement wrapped in a fragment that will render a footer
tag. In this footer
tag, I have previousPost
, and if the post does exist, we display that previous post title
.
Using next/link
, the user has access to a clickable link to route them to that previous post page. Otherwise, if we do not have a previous post, it will render null
, and nothing will appear.
After that, we have a similar JSX
called nextPost
which does the exact same thing as previousPost
except it will show and render the next post.
return (
<>
<footer style={{ display: "flex", textAlign: "center" }}>
{previousPost ? (
<div
style={{
border: "2px solid #ddd",
padding: "1rem",
}}
>
<Link href={`${previousPost.slug}`}>
<a>👈 {previousPost.title}</a>
</Link>
</div>
) : null}
{nextPost ? (
<div
style={{
border: "2px solid #ddd",
padding: "1rem",
marginLeft: "1rem",
}}
>
<Link href={`${nextPost.slug}`}>
<a>{nextPost.title} 👉</a>
</Link>
</div>
) : null}
</footer>
</>
);
}
This component is now ready to be embedded into a single-page
WP template.
Faust.js WordPress templates
Faust.js provides a JavaScript version of the WordPress template hierarchy and its system. Let’s utilize this system for our paginated footer component with the single.js
file which is the WP template component that renders the single-post details page.
Navigating to the wp-templates/single.js
, copy and paste over the current boilerplate code already in this file with this block:
import { gql } from "@apollo/client";
import PaginatedFooter from "../components/Footer/PaginatedFooter";
import * as MENUS from "../constants/menus";
import { BlogInfoFragment } from "../fragments/GeneralSettings";
import {
Header,
Main,
Container,
EntryHeader,
NavigationMenu,
ContentWrapper,
FeaturedImage,
SEO,
} from "../components";
export default function Component(props) {
// Loading state for previews
if (props.loading) {
return <>Loading...</>;
}
const { title: siteTitle, description: siteDescription } =
props?.data?.generalSettings;
const primaryMenu = props?.data?.headerMenuItems?.nodes ?? [];
const footerMenu = props?.data?.footerMenuItems?.nodes ?? [];
const { title, content, featuredImage, date, author } = props.data.post;
return (
<>
<SEO
title={siteTitle}
description={siteDescription}
imageUrl={featuredImage?.node?.sourceUrl}
/>
<Header title={siteTitle} description={siteDescription} />
<Main>
<>
<EntryHeader
title={title}
image={featuredImage?.node}
date={date}
author={author?.node?.name}
/>
<Container>
<ContentWrapper content={content} />
</Container>
</>
</Main>
<PaginatedFooter post={props.data.post} />
</>
);
}
Component.query = gql`
${BlogInfoFragment}
${NavigationMenu.fragments.entry}
${FeaturedImage.fragments.entry}
query GetPost(
$databaseId: ID!
$headerLocation: MenuLocationEnum
$footerLocation: MenuLocationEnum
$asPreview: Boolean = false
) {
post(id: $databaseId, idType: DATABASE_ID, asPreview: $asPreview) {
title
content
date
author {
node {
name
}
}
previousPost {
title
slug
}
nextPost {
title
slug
}
...FeaturedImageFragment
}
generalSettings {
...BlogInfoFragment
}
headerMenuItems: menuItems(where: { location: $headerLocation }) {
nodes {
...NavigationMenuItemFragment
}
}
footerMenuItems: menuItems(where: { location: $footerLocation }) {
nodes {
...NavigationMenuItemFragment
}
}
}
`;
Component.variables = ({ databaseId }, ctx) => {
return {
databaseId,
headerLocation: MENUS.PRIMARY_LOCATION,
footerLocation: MENUS.FOOTER_LOCATION,
asPreview: ctx?.asPreview,
};
};
There are 109 lines of code in this example so let’s break this down at a high level and then I shall focus on the changes I made to the actual boilerplate starter code with my own customization.
The templates in Faust as you see in the code block have 3 main parts: Component, Query, and Variable. Please refer to the Faust docs to get a deeper explanation of what each does.
Let’s start by focusing on the query layer of the template. At the bottom of the file, I have customized the query to ask for the previousPost
and nextPost
fields as shown:
Component.query = gql`
${BlogInfoFragment}
${NavigationMenu.fragments.entry}
${FeaturedImage.fragments.entry}
query GetPost(
$databaseId: ID!
$headerLocation: MenuLocationEnum
$footerLocation: MenuLocationEnum
$asPreview: Boolean = false
) {
post(id: $databaseId, idType: DATABASE_ID, asPreview: $asPreview) {
title
content
date
author {
node {
name
}
}
previousPost {
title
slug
}
nextPost {
title
slug
}
The variables are already set to what I need, so the last thing I have to do is go back up to the top of the component which is the rendering layer and add the PaginatedFooter
component after importing it at the top within the return statement. Then I can pass in and destructure the post props
data in the PaginatedFooter
component:
return (
<>
<SEO
title={siteTitle}
description={siteDescription}
imageUrl={featuredImage?.node?.sourceUrl}
/>
<Header title={siteTitle} description={siteDescription} />
<Main>
<>
<EntryHeader
title={title}
image={featuredImage?.node}
date={date}
author={author?.node?.name}
/>
<Container>
<ContentWrapper content={content} />
</Container>
</>
</Main>
<PaginatedFooter post={props.data.post} />
</>
);
Super Stoked! First, run npm run generate
for the types and then run this on the dev server. Let’s see this now work on the browser:
Conclusion 🚀
Faust.js and its new GraphQL client Apollo give developers a better way to develop headless WordPress.
I hope you have a better understanding of how to work with Apollo in Faust.js. As always, super stoked to hear your feedback and any questions you might have on headless WordPress. Hit us up in our discord!
Top comments (1)
What is the benefit of Apollo client? diamond exch id