Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

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. TheApollo 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 theFaust 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 modeExit fullscreen mode

Let’s go over what is happening in this file. At the top of the file,graphql and theuseQuery hook from Apollo is being imported into the file. Then we have a default function calledPage that will render thetitle anddescription of the general settings from your WPGraphQL API. After passing the props into thePage function, we create a variable that contains thedata object that is the destructured response from theuseQuery hook provided by Apollo. This then takesPage.query as a parameter.

At the bottom of the file is our actual GraphQL query whichPage.query is coming from as a parameter. We are calling in the constant variable with theuseQuery hook.

Back above the return statement, we have an object that contains the destructured fields queried which are thetitle anddescription from the data in WordPressgeneralSettings.

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 onPagination in Headless WP.

The first thing we need to do is go into thecomponents folder at the root of the Faust project. In the components folder, create a file calledLoadMorePost.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 modeExit fullscreen mode

Let’s break this file down into chunks. At the top of the file, I am importing theuseQuery hook andgql provided by the Apollo client that I am using as well asnext/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 modeExit fullscreen mode

After that, I have a default components function calledLoadMorePost. In this function, I am utilizing theuseQuery hook in Apollo to pass in my query calledGET_POSTS from the top of the file.

Next, I have variables that I pass in, which is thebatch size I defined to be5 , 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 modeExit fullscreen mode

There are 2 variables that get set next. The first variable isposts which is taking the data that Apollo gives us back and drilling down into it with theposts and their nested data. The second variable ishaveMorePosts which checks if we have more posts to load but if there are no moreposts 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 modeExit 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 adatabaseId,title, and itsslug. 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 modeExit 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 ourhaveMorePosts 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 aonSubmit handler that calls thefetchMorefunction 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 modeExit 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 calledpagination.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 modeExit 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 implementrelay-style pagination with the relay spec using merge and read functions, which means all the details ofconnections,edges andpageInfo 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 thatfunction from Apollo.

In order tocreate a Faust plugin, we are going to use itsapply method which is a JavaScript class. Theapply method has a parameter calledhooks which is passed from@wordpress/hooks.

The first step is to go to the root of the project and create a folder calledplugins. In this plugin folder, create a file calledRelayStylePagination.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 modeExit fullscreen mode

At the top of the file, we import therelayStylePagination function from Apollo’s utility library. Following that, we create aclass component which is the basic syntax used in a Faust plugin.

Next, we have anapply method with thehooks parameter which is an object that gives you a function calledaddFilter.TheaddFilter function allows us to modify the Apollo Client’sInMemoryCacheOptions.

In the next few lines of code, we are taking theaddFilter hook and calling thememory cache functionoptions. Theoptions are coming from theApollo Client Cache configuration. Theseoptions allow for configuring the cache’s behavior to suit our use case. In this article, we are defining the configuration for pagination. Thefaust namespace follows that.

Next, we spread through ouroptions which is a callback function that can return theinMem cache options as they are. We can also merge in our own withtypePolicies that we define along with other specific queries we need to merge in the future.ThetypePolicies is a class that defines a policy of your type in Apollo. Here, we are adding it to theRootQuery option:

  addFilter("apolloClientInMemoryCacheOptions", "faust", (options) => {      return {        ...options,        typePolicies: {          ...options.typePolicies,          RootQuery: {            ...options.typePolicies.RootQuery
Enter fullscreen modeExit fullscreen mode

The last few lines of code are where we are defining our fields of theposts 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 therelayStylePagination function provided. Another thing to note is I also addedContentType to expose thecontentNodes fields in case of using something like search functionality.

   fields: {              posts: relayStylePagination(),            },          },          ContentType: {            fields: {              contentNodes: relayStylePagination(),            },          },        },      };    });  }}
Enter fullscreen modeExit fullscreen mode

Lastly, we need to go to ourfaust.config.js file to imbed it into theexperimentalPlugins 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 modeExit fullscreen mode

Generating possible types in Apollo

Before you run the dev server, make sure you type the commandnpm run generate since we updated the schema. The Apollo client requires that we provide apossibleTypes 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 forprevious andnext 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 theslug 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 modeExit 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 theFooter folder and create a file calledPaginatedFooter.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 modeExit fullscreen mode

At the top of the file, I am importingLink fromnext/link using Next.js’s client-side navigation to link pages.

Then I have a default function calledPaginatedFooter that is accepting thepost data as its props. The following is a constant variable that destructures thepost props ofpreviousPost andnextPost which we now have exposed and are querying from WPGraphQL.

export default function PaginatedFooter({ post }) {  const { previousPost, nextPost } = post;
Enter fullscreen modeExit fullscreen mode

Lastly, I have a return statement wrapped in a fragment that will render afooter tag. In thisfooter tag, I havepreviousPost, and if the post does exist, we display that previouspost title.

Usingnext/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 rendernull, and nothing will appear.

After that, we have a similarJSX callednextPost which does the exact same thing aspreviousPost 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 modeExit fullscreen mode

This component is now ready to be embedded into asingle-page WP template.

Faust.js WordPress templates

Faust.js provides a JavaScript version of theWordPress template hierarchy and its system. Let’s utilize this system for our paginated footer component with thesingle.js file which is the WP template component that renders the single-post details page.

Navigating to thewp-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 modeExit 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 theFaust 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 thepreviousPost andnextPost 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 modeExit 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 thePaginatedFooter component after importing it at the top within the return statement. Then I can pass in and destructure thepost props data in thePaginatedFooter 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 modeExit fullscreen mode

Super Stoked! First, runnpm 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 ourdiscord!

Top comments(1)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
sallybauer profile image
SallyBauer
Hello I am a coder and I am proficient in HTML, CSS, Javascript. And we love reading about new programming techniques.
  • Location
    Wheeling, WV 26003
  • Joined

What is the benefit of Apollo client?diamond exch id

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Rock Climber•Jamstack/ Decoupled Nerd•Full Stack Dev Alum @Covalence_io, • Dev Advocate Engineer Headless WordPress @wpengine • formerly @Netlify
  • Location
    Texas
  • Joined

More fromFran Agulto

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp