在组件内获取 Next.js 数据的最佳实践

IT技术 javascript reactjs next.js apollo-client
2021-03-31 02:36:57

我有一个全局显示的菜单组件。将数据导入该组件的最佳实践是什么?

我正在尝试利用 Next.js 提供的静态生成,但Next.js 团队的所有数据获取指南与页面相关。getStaticProps并且getStaticPaths似乎与页面生成有关,而不是组件数据。他们的SWR包裹是正确的答案,还是 Apollo Client?

通常在基于钩子的 React 中,我只是将我的数据调用放入useEffect. 我不知道如何解释这一点,因为一切都是在构建时使用 Next.js 渲染的。

2个回答

这是一个非常棘手的问题,我认为我们需要在解决方案成为焦点之前布置一些背景。我专注于 React.js 世界,但其中很多都适用于我想象的 Vue/Nuxt。

背景/静态生成的好处:

Gatsby 和 Next 专注于生成静态页面,这极大地提高了 React.js 站点的性能和 SEO。除了这个简单的见解之外,这两个平台还有很多技术开销,但让我们从数字机器为浏览器抽出花哨的 HTML 页面的想法开始。

页面数据获取

对于 Next.js(截至v9.5),它们的数据获取机制getStaticProps为您完成了大部分繁重的工作,但它被沙盒化到/pages/目录中。这个想法是它会为你获取数据,并在构建时告诉 Node 中的 Next.js 页面生成器(而不是在useEffect钩子中在组件端执行它- 或componentDidMount)。Gatsby 对他们的gatsby-node.js文件做了很多相同的事情,它与 Node 服务器协调页面构建的数据获取。

需要数据的全局组件呢?

您可以同时使用 Gatsby 和 Next 来生成任何类型的网站,但一个巨大的用例是 CMS 驱动的网站,因为其中大部分内容都是静态的。这些工具非常适合该用例。

在典型的 CMS 站点中,您将拥有全局元素——页眉、页脚、搜索、菜单等。这是静态生成面临的一个巨大挑战:如何在构建时将数据放入动态全局组件中?这个问题的答案是……你没有。如果你想一想,这是有道理的。如果您有一个 10K 页面的站点,如果有人向菜单添加新的导航项,您是否希望触发站点范围的重建?

全局组件的数据获取

那么我们如何解决这个问题呢?我的最佳答案是apollo-client并执行获取客户端。这对我们有帮助的原因有很多:

  • 对于小型查询,性能影响可以忽略不计。
  • 如果我们需要为 CMS 层的更改重建页面,这会通过 Next/Gatsby 的检测机制进行滑动,因此我们可以进行全局更改而不会触发巨大的站点范围的重建。

那么这实际上是什么样子的呢?在组件级别,它看起来就像一个普通的 Apollo 增强组件。我通常使用,styled-components但我试图把它去掉,这样你就可以更好地看到发生了什么。

import React from 'react'
import { useQuery, gql } from '@apollo/client'
import close from '../public/close.svg'

/**
 * <NavMenu>
 * 
 * Just a typical menu you might see on a CMS-driven site. It takes in a couple of props to move state around.
 * 
 * @param { boolean } menuState - lifted state true/false toggle for menu opening/closing
 * @param { function } handleMenu - lifted state changer for menuState, handles click event
 */

const NAV_MENU_DATA = gql`
  query NavMenu($uid: String!, $lang: String!) {
    nav_menu(uid: $uid, lang: $lang) {
      main_menu_items {
        item {
          ... on Landing_page {
            title
            _linkType
            _meta {
              uid
              id
            }
          }
        }
      }
    }
  }
`

const NavMenu = ({ menuState, handleMenu }) => {
  // Query for nav menu from Apollo, this is where you pass in your GraphQL variables
  const { loading, error, data } = useQuery(NAV_MENU_DATA, {
    variables: {
      "uid": "nav-menu",
      "lang": "en-us"
    }
  })
  
  if (loading) return `<p>Loading...</p>`;
  if (error) return `Error! ${error}`;

  // Destructuring the data object
  const { nav_menu: { main_menu_items } } = data

  // `menuState` checks just make sure out menu was turned on
  if (data) return(
    <>
      <section menuState={ menuState }>
        <div>
          { menuState === true && (
            <div>Explore</div>
          )}
          <div onClick={ handleMenu }>
          { menuState === true && (
            <svg src={ close } />
          )}
          </div>
        </div>
        { menuState === true && (
          <ul>
            { data.map( (item) => {
              return (
                <li link={ item }>
                  { item.title }
                </li>
              )
            })}
          </ul>
        )}
      </section>
    </>
  )
}

export default NavMenu

设置 Next 以使用 Apollo

这实际上由 Next.js 团队记录得非常好,这让我觉得我并没有完全破坏这个工具应该工作的方式。你可以在他们的 repo 中找到使用 Apollo 的很好的例子

将 Apollo 引入 Next 应用的步骤:

  1. 制作一个自定义useApollo挂钩来设置与数据源的连接(我将我的挂钩放在/lib/apollo/apolloClient.jsNext 的层次结构中,但我确定它可以放在其他地方)。
import { useMemo } from 'react'
import { ApolloClient, InMemoryCache, SchemaLink, HttpLink } from '@apollo/client'

let apolloClient

// This is mostly from next.js official repo on how best to integrate Next and Apollo
function createIsomorphLink() {
  // only if you need to do auth
  if (typeof window === 'undefined') {
    // return new SchemaLink({ schema }) 
    return null
  } 
  // This sets up the connection to your endpoint, will vary widely.
  else {
    return new HttpLink({
      uri: `https://yourendpoint.io/graphql`
    })
  }
}

// Function that leverages ApolloClient setup, you could just use this and skip the above function if you aren't doing any authenticated routes
function createApolloClient() {
  return new ApolloClient({
    ssrMode: typeof window === 'undefined',
    link: createIsomorphLink(),
    cache: new InMemoryCache(),
  })
}


export function initializeApollo(initialState = null) {
  const _apolloClient = apolloClient ?? createApolloClient()

  // If your page has Next.js data fetching methods that use Apollo Client, the initial state
  // gets hydrated here
  if (initialState) {
    // Get existing cache, loaded during client side data fetching
    const existingCache = _apolloClient.extract()
    // Restore the cache using the data passed from getStaticProps/getServerSideProps
    // combined with the existing cached data
    _apolloClient.cache.restore({ ...existingCache, ...initialState })
  }
  // For SSG and SSR always create a new Apollo Client
  if (typeof window === 'undefined') return _apolloClient
  // Create the Apollo Client once in the client
  if (!apolloClient) apolloClient = _apolloClient

  return _apolloClient
}

// This is goal, now we have a custom hook we can use to set up Apollo across our app. Make sure to export this!
export function useApollo(initialState) {
  const store = useMemo(() => initializeApollo(initialState), [initialState])
  return store
}
  1. _app.js/pages/Next目录下修改这基本上是围绕 Next 中每个页面的包装器。我们将向其中添加 Apollo 提供者,现在我们可以从任何组件全局访问 Apollo。
import { ApolloProvider } from '@apollo/react-hooks'
import { useApollo } from '../lib/apollo/apolloClient'

/**
 * <MyApp>
 * 
 * This is an override of the default _app.js setup Next.js uses
 * 
 * <ApolloProvider> gives components global access to GraphQL data fetched in the components (like menus)
 * 
 */
const MyApp = ({ Component, pageProps }) => {
  // Instantiates Apollo client, reads Next.js props and initialized Apollo with them - this caches data into Apollo.
  const apolloClient = useApollo(pageProps.initialApolloState)

  return (
    <ApolloProvider client={ apolloClient }>
      <Component {...pageProps} />
    </ApolloProvider>
  )
}

export default MyApp

现在您可以使用 Apollo 获取组件内部的动态数据!很简单吧;) 哈!

@serraosays 你说得有点多,我已经在生产中重构了它。我这样做是因为它用于单页网站。
2021-05-24 02:36:57
为什么SchemaLink在这里注释掉?它非常有用。
2021-05-31 02:36:57
@Mark - 您在 Next 中导入页面的所有 React 组件都遵循您使用getStaticProps设置的生成策略getServerProps它们几乎总是从服务器端交付。Apollo 根据客户端请求获取数据,使其保持最新状态
2021-06-04 02:36:57
@serraosays 谢谢!目前我正在使用另一种方法/解决方法,即通过将道具传递给组件。我不确定这是否是好的做法:github.com/markdost/next.js-wp-posts/blob/main/index.example.js
2021-06-07 02:36:57
我在你的例子中没有得到的是NavMenu客户端、SSG 还是 SSR 中的数据?
2021-06-12 02:36:57

对于 NextJS 中的全局数据获取,我使用react-query并且不需要全局状态,因为它允许您缓存数据。假设您有一个包含类别的博客,并且您想将类别名称放在navbar下拉菜单中。在这种情况下,您可以调用 APIreact-querynavbar组件中获取数据并缓存它。导航栏数据可用于所有页面。