wpengine / faustjs

Faust.js™ - The Headless WordPress Framework
https://faustjs.org
Other
1.44k stars 131 forks source link

Persistent navigation in a Next.js _app #935

Closed roeean closed 2 years ago

roeean commented 2 years ago

Hello,

First, thank you so much for the fantastic package you created! It has drastically improved the performance of my project and greatly facilitated the development and maintenance process.

I'm in the middle of building a Next.js app, and I need to make a request for the navigation content to my WPGraphQL, get the results, and pass them to my Header component so that I can have a persistent navigation menu. I don't want to include the header in every page component, nor do I want to have to query the document on every page.

Given that it is impossible to access getServerSideProps and getStaticProp within my _app.js, my only option is to add getInitialProps.

However, when I try to use faust.js in the _app file, I get the following error, probably because the request is sent before the initialization of the faust-client.

Do you have any idea how to solve this?

Thank you in advance,

my _app.js code:

import 'faust.config';
import 'styles/globals.scss';

import { FaustProvider } from '@faustjs/next';
import { client } from 'client';

import { Header } from 'components/layout/header';

function MyApp ({ Component, pageProps, menu }) {
  return (
    <FaustProvider client={client} pageProps={pageProps}>
      <Header menu={menu} />
      <Component {...pageProps} />
    </FaustProvider>
  );
}

MyApp.getInitialProps = async (ctx) => {
  const { useQuery } = client;
  const { menuItems } = useQuery();
  const menu = menuItems({
    where: { location: 'PRIMARY' }
  }).nodes;

  return { menu };
};

export default MyApp;

the error:

image

theodesp commented 2 years ago

@roeean The problem with getInitialProps is that they are called on the server side so, there is no client side context. In this case you cannot use any hooks there.

Also bear in mind that getInitialProps will disable Automatic Static Optimization.

If you want to fetch the menus, GQty will handle that for you at build time so there is no need to use getInitialProps.

Take a look at the relevant component:

https://github.com/wpengine/faustjs/blob/81fcbb12d4699e7809965dfe7128e66e1485299d/examples/next/getting-started/src/components/Header.tsx#L15-L18

When you load the page you will see that there are no client side requests happening. The request happens once on the server at build time.

What you may want to test is having a common layout component as described in this post:

https://nextjs.org/docs/basic-features/layouts

If your page props do not change then the header will not re-render as well.

Additionally you could just create a header component, use the useQuery hook to get the menus and render them in place. This is what we do in the example/getting-started project, although we require the generalSettings info

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <>
      <FaustProvider client={client} pageProps={pageProps}>
        <Header />
        <Component {...pageProps} />
      </FaustProvider>
    </>
  );
}
richardcalahan commented 2 years ago

@theodesp If you were to move <Header /> outside of the <Component /> tree, as you have in your last snippet, each call to useQuery from within <Header /> would happen on the client side. It seems only calls to useQuery from within a page (that uses getNextStaticProps) will be statically cached.

Correct me if I'm wrong?

If I'm correct, is there a way to do what you've suggested in your last snippet? To have a global layout that exists outside of the rendered page (headers, footers) that request their own data that is statically cached at build/regen time?

theodesp commented 2 years ago

@richardcalahan I think gqty does capture the requests happening in the server side so there are no CSR requests happening by default. If you look at the getting-started example project all of the content is rendered statically with 900 seconds invalidation: https://faustjs.org/docs/next/guides/ssr-ssg#setting-up-incremental-static-regeneration-isr

You can verify that when you do a production build and then run the next export which creates static html files. For example this is the static html generated at build time for the getting-started example /posts page:

Screenshot 2022-07-28 at 12 59 56
<!DOCTYPE html>
<html>
  <head>
    <meta charSet="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>myBlog - Just another WordPress site</title>
    <meta name="next-head-count" content="3" />
    <link rel="preload" href="/_next/static/css/5c54c503f8732a50.css" as="style" />
    <link rel="stylesheet" href="/_next/static/css/5c54c503f8732a50.css" data-n-g="" />
    <link rel="preload" href="/_next/static/css/f550fd9896487ad8.css" as="style" />
    <link rel="stylesheet" href="/_next/static/css/f550fd9896487ad8.css" data-n-p="" />
    <noscript data-n-css=""></noscript>
    <script defer="" nomodule="" src="/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js"></script>
    <script src="/_next/static/chunks/webpack-3997a95a90267402.js" defer=""></script>
    <script src="/_next/static/chunks/framework-5f4595e5518b5600.js" defer=""></script>
    <script src="/_next/static/chunks/main-1038209226d7adb4.js" defer=""></script>
    <script src="/_next/static/chunks/pages/_app-2e4cb5b4461bafa1.js" defer=""></script>
    <script src="/_next/static/chunks/250-041c501ac42cc543.js" defer=""></script>
    <script src="/_next/static/chunks/pages/posts-83c45a42c1083aa5.js" defer=""></script>
    <script src="/_next/static/pt62k4m3DC6gM95ft__vB/_buildManifest.js" defer=""></script>
    <script src="/_next/static/pt62k4m3DC6gM95ft__vB/_ssgManifest.js" defer=""></script>
  </head>
  <body>
    <div id="__next" data-reactroot="">
      <header>
        <div class="Header_wrap__R9imJ">
          <div class="Header_title-wrap__4e17x">
            <p class="Header_site-title__Otw1K">
              <a href="/">myBlog</a>
            </p>
            <p class="Header_description__EJ3Db">Just another WordPress site</p>
          </div>
          <div class="Header_menu__6BMG0">
            <ul>
              <li>
                <a href="http://localhost:3000/sample-page/">Sample Page</a>
              </li>
              <li>
                <a class="button" href="https://github.com/wpengine/faustjs">GitHub</a>
              </li>
            </ul>
          </div>
        </div>
      </header>
      <main class="content content-index">
        <section id="posts_post_list__glOzP">
          <div class="wrap">
            <h2>Blog Posts</h2>
            <div class="posts">
              <div class="Posts_single__rpTye" id="post-cG9zdDoyMw==">
                <div>
                  <h3 class="Posts_title__q_vy3">
                    <a href="/posts/example-post">Example Post</a>
                  </h3>
                  <div>
                    <p>Hello This is a Header Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, [&hellip;]</p>
                  </div>
                  <a aria-label="Read more about function ProxyFn(argValues = emptyVariablesObject) {
              return resolve({
                argValues,
                argTypes: __args
              });
            }" href="/posts/example-post">Read more</a>
                </div>
              </div>
              <div class="Posts_single__rpTye" id="post-cG9zdDox">
                <div>
                  <h3 class="Posts_title__q_vy3">
                    <a href="/posts/hello-world">Hello world!</a>
                  </h3>
                  <div>
                    <p>Welcome to WordPress. This is your first post. Edit or delete it, then start writing! Welcome to WordPress. This is your first post. Edit or delete it, then start writing! Heading Hello</p>
                  </div>
                  <a aria-label="Read more about function ProxyFn(argValues = emptyVariablesObject) {
              return resolve({
                argValues,
                argTypes: __args
              });
            }" href="/posts/hello-world">Read more</a>
                </div>
              </div>
            </div>
          </div>
        </section>
        <nav class="pagination" aria-label="Pagination">
          <div class="wrap">
            <ul></ul>
          </div>
        </nav>
      </main>
      <footer class="Footer_main__SP1FG">
        <div class="Footer_wrap__4e6e3">
          <p>© 2022 myBlog. All rights reserved.</p>
        </div>
      </footer>
    </div>
    <script id="__NEXT_DATA__" type="application/json">
      {
        "props": {
          "pageProps": {
            "__CLIENT_CACHE_PROP": "{\"cache\":{\"generalSettings\":{\"__typename\":\"GeneralSettings\",\"title\":\"myBlog\",\"description\":\"Just another WordPress site\"},\"menuItems_8d665_12a43\":{\"__typename\":\"RootQueryToMenuItemConnection\",\"nodes\":[{\"__typename\":\"MenuItem\",\"id\":\"cG9zdDoxNw==\",\"url\":\"http://localhost:3000/sample-page/\",\"label\":\"Sample Page\"}]},\"posts_f30bc_81560\":{\"__typename\":\"RootQueryToPostConnection\",\"nodes\":[{\"__typename\":\"Post\",\"id\":\"cG9zdDoyMw==\",\"slug\":\"example-post\",\"title_b28e9_bf21a\":\"Example Post\",\"excerpt_b28e9_bf21a\":\"\u003cp\u003eHello This is a Header Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, [\u0026hellip;]\u003c/p\u003e\\n\"},{\"__typename\":\"Post\",\"id\":\"cG9zdDox\",\"slug\":\"hello-world\",\"title_b28e9_bf21a\":\"Hello world!\",\"excerpt_b28e9_bf21a\":\"\u003cp\u003eWelcome to WordPress. This is your first post. Edit or delete it, then start writing! Welcome to WordPress. This is your first post. Edit or delete it, then start writing! Heading Hello\u003c/p\u003e\\n\"}],\"pageInfo\":{\"__typename\":\"WPPageInfo\",\"startCursor\":\"YXJyYXljb25uZWN0aW9uOjIz\",\"endCursor\":\"YXJyYXljb25uZWN0aW9uOjE=\",\"hasPreviousPage\":false,\"hasNextPage\":false}}},\"selections\":[[[\"{\\\"after\\\":\\\"String\\\",\\\"before\\\":\\\"String\\\",\\\"first\\\":\\\"Int\\\",\\\"last\\\":\\\"Int\\\",\\\"where\\\":\\\"RootQueryToPostConnectionWhereArgs\\\"}\",\"f30bc\"],[\"{\\\"first\\\":6}\",\"81560\"],[\"{\\\"after\\\":\\\"String\\\",\\\"before\\\":\\\"String\\\",\\\"first\\\":\\\"Int\\\",\\\"last\\\":\\\"Int\\\",\\\"where\\\":\\\"RootQueryToMenuItemConnectionWhereArgs\\\"}\",\"8d665\"],[\"{\\\"where\\\":{\\\"location\\\":\\\"PRIMARY\\\"}}\",\"12a43\"],[\"{\\\"format\\\":\\\"PostObjectFieldFormatEnum\\\"}\",\"b28e9\"],[\"{}\",\"bf21a\"]],\"v1\"]}",
            "__AUTH_CLIENT_CACHE_PROP": "{}"
          },
          "__N_SSG": true
        },
        "page": "/posts",
        "query": {},
        "buildId": "pt62k4m3DC6gM95ft__vB",
        "isFallback": false,
        "gsp": true,
        "scriptLoader": []
      }
    </script>
  </body>
</html>

The menu items there are compiled at build time.

richardcalahan commented 2 years ago

@theodesp correct, but in your last snippet i was referencing you suggested moving the <Header /> component out of the page and into the root of the <FaustProvider /> in _app.js. I'd like to do something like that, but once you do the useQuery call only happens on the frontend.

The getting-started example has the <Header /> component in the page, which ensures its query is compiled at build time.

theodesp commented 2 years ago

@richardcalahan yes you are right sorry my bad. Within the _app.tsx component you cannot use getStaticProps as it is not a page component and this is what Next.js mentions when you want to use data fetching in layouts https://nextjs.org/docs/basic-features/layouts#data-fetching. In order to avoid this issue you can either:

  1. Use a custom App as mentioned in this section: https://nextjs.org/docs/advanced-features/custom-app. You won't be able to use GQty hooks there, but you can use the fetch api sending https://gqty.dev/docs/client/fetching-data#resolved

Here is a rough implementation:

import 'faust.config';
import { FaustProvider } from '@faustjs/next';
import 'normalize.css/normalize.css';
import React from 'react';
import 'scss/main.scss';
import { client } from 'client';
import type { AppProps } from 'next/app';

export default function MyApp({ Component, pageProps, menus }: AppProps) {
  return (
    <>
      <FaustProvider client={client} pageProps={pageProps}>
        <Component {...pageProps} />
      </FaustProvider>
    </>
  );
}

MyApp.getInitialProps = async (ctx) => {
  const res = await fetchMenusJSON();
  return { menus: res?.data?.menuItems ?? {} };
};

async function fetchMenusJSON() {
  const response = await fetch(`https://headlessfw.wpengine.com/graphql`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: `
        query MenusQuery {
          menuItems(where: {location: PRIMARY}) {
            nodes {
              id
              url
              label
            }
          }
        }
        `,
    })
  });
  const menus = await response.json();
  return menus;
}
  1. Create a custom script that preloads all the menu items at build time and stores them in the global window as a json file. Then feed that data to the Header menu. Take a look at this example script that we use for fetching all the possible graphql types: https://github.com/wpengine/faustjs/pull/951/files#diff-352dac9ba523c97ac5ad3fac305c7f20f90b4a271913e3c89d375b908b4401c3 . The setback here is that you can only run this at build time and if you change the menus on the Wordpress side you won't be able to see the latest changes.

I hope this helps.

richardcalahan commented 2 years ago

Cool, yeah we've used both methods here and there in the past. They work well. Thanks for the help.

theodesp commented 2 years ago

@roeean did the above options solved your issues yet?

roeean commented 2 years ago

@theodesp, Yes, the issue is solved, Thank you.