DEV Community

Fran Agulto
Fran Agulto

Posted on

Working with the Apollo Client in Faust.js

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
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

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>
      )}
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>;
  }

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
      )}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

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(),
            },
          },
        },
      };
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(),
            },
          },
        },
      };
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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!!

Image description

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
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

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,
  };
};
Enter fullscreen mode Exit fullscreen mode

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
      }

Enter fullscreen mode Exit fullscreen mode

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} />
    </>
  );


Enter fullscreen mode Exit fullscreen mode

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:

Image description

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)

Collapse
 
sallybauer profile image
SallyBauer

What is the benefit of Apollo client? diamond exch id