Next.js 在组件内获取数据的最佳实践
Best practice for Next.js data fetching inside a component
我有一个全局显示的菜单组件。将数据导入该组件的最佳做法是什么?
我正在尝试利用 Next.js 提供的静态生成,但 Next.js 团队的所有 data fetching 指南都与页面相关。 getStaticProps
和 getStaticPaths
似乎与页面生成有关,而不是组件数据。他们的 SWR
包是正确答案,还是 Apollo Client?
通常在基于钩子的 React 中,我只是将数据调用放入 useEffect
。我不确定如何推断出所有内容都是在构建时使用 Next 呈现的。
这是一个棘手的问题,我认为我们需要先了解一些背景知识,然后才能确定解决方案。我专注于 React.js 世界,但其中很多都适用于我想象的 Vue/Nuxt。
后台/静态生成优势:
Gatsby 和 Next 专注于生成静态页面,这极大地提高了 React.js 站点的性能和 SEO。除了这个简单的洞察力之外,这两个平台都有很多技术开销,但让我们从这个数字机器为浏览器抽出花哨的 HTML 页面的想法开始。
页面数据获取
在 Next.js 的情况下(从 v9.5
开始),他们的数据获取机制 getStaticProps
为您完成了大部分繁重的工作,但它被沙盒化到 /pages/
目录。这个想法是它为您获取数据并在构建期间告诉 Node 中的 Next.js 页面生成器(而不是在 useEffect
挂钩中 component-side - 或 componentDidMount
). Gatsby 对他们的 gatsby-node.js
文件做了很多相同的事情,该文件与 Node 服务器协调页面构建的数据获取。
需要数据的全局组件呢?
您可以同时使用 Gatsby 和 Next 来制作任何类型的网站,但是 CMS-driven 网站是一个巨大的用例,因为其中很多内容都是静态的。这些工具非常适合该用例。
在典型的 CMS 站点中,您将拥有全局元素 - 页眉、页脚、搜索、菜单等。这就是静态生成面临的一大挑战:如何在构建时将数据放入动态全局组件中?这个问题的答案是……你不知道。如果你想一想,这是有道理的。如果您有一个 10K 页面的站点,如果有人向菜单添加新的导航项,您是否要触发 site-wide 重建?
全局组件的数据获取
那么我们如何解决这个问题呢?我得到的最佳答案是 apollo-client
并执行获取客户端。出于多种原因,这对我们有帮助:
- 对于小型查询,性能影响可以忽略不计。
- 如果我们需要为 CMS 层的更改重建页面,这会通过 Next/Gatsby 的检测机制滑动,因此我们可以在不触发巨大的 site-wide 重建的情况下进行全局更改。
那么这到底是什么样子的呢?在组件级别,它看起来就像一个常规的 Apollo-enhanced 组件。我通常使用 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
为下一步使用 Apollo 进行设置
Next.js 团队实际上很好地记录了这一点,这让我觉得我并没有完全破解这个工具的工作方式。你可以在他们的仓库中找到 great examples of using Apollo。
让 Apollo 进入下一个应用程序的步骤:
- 创建一个自定义
useApollo
挂钩来设置与您的数据源的连接(我将我的挂钩放在 Next 层次结构中的 /lib/apollo/apolloClient.js
中,但我相信它可以放在其他地方)。
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
}
- 在Next的
/pages/
目录下修改_app.js
。这基本上是 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 获取组件内部的动态数据!太简单了 ;) 哈!
对于 NextJS 中的全局数据获取,我使用 react-query
并且不需要全局状态,因为它可以让您缓存数据。假设您有一个包含类别的博客,并且您希望将类别名称作为下拉菜单放在 navbar
中。在这种情况下,您可以调用 API 从 navbar
组件中获取带有 react-query
的数据并将其缓存。导航栏数据将适用于所有页面。
我有一个全局显示的菜单组件。将数据导入该组件的最佳做法是什么?
我正在尝试利用 Next.js 提供的静态生成,但 Next.js 团队的所有 data fetching 指南都与页面相关。 getStaticProps
和 getStaticPaths
似乎与页面生成有关,而不是组件数据。他们的 SWR
包是正确答案,还是 Apollo Client?
通常在基于钩子的 React 中,我只是将数据调用放入 useEffect
。我不确定如何推断出所有内容都是在构建时使用 Next 呈现的。
这是一个棘手的问题,我认为我们需要先了解一些背景知识,然后才能确定解决方案。我专注于 React.js 世界,但其中很多都适用于我想象的 Vue/Nuxt。
后台/静态生成优势:
Gatsby 和 Next 专注于生成静态页面,这极大地提高了 React.js 站点的性能和 SEO。除了这个简单的洞察力之外,这两个平台都有很多技术开销,但让我们从这个数字机器为浏览器抽出花哨的 HTML 页面的想法开始。
页面数据获取
在 Next.js 的情况下(从 v9.5
开始),他们的数据获取机制 getStaticProps
为您完成了大部分繁重的工作,但它被沙盒化到 /pages/
目录。这个想法是它为您获取数据并在构建期间告诉 Node 中的 Next.js 页面生成器(而不是在 useEffect
挂钩中 component-side - 或 componentDidMount
). Gatsby 对他们的 gatsby-node.js
文件做了很多相同的事情,该文件与 Node 服务器协调页面构建的数据获取。
需要数据的全局组件呢?
您可以同时使用 Gatsby 和 Next 来制作任何类型的网站,但是 CMS-driven 网站是一个巨大的用例,因为其中很多内容都是静态的。这些工具非常适合该用例。
在典型的 CMS 站点中,您将拥有全局元素 - 页眉、页脚、搜索、菜单等。这就是静态生成面临的一大挑战:如何在构建时将数据放入动态全局组件中?这个问题的答案是……你不知道。如果你想一想,这是有道理的。如果您有一个 10K 页面的站点,如果有人向菜单添加新的导航项,您是否要触发 site-wide 重建?
全局组件的数据获取
那么我们如何解决这个问题呢?我得到的最佳答案是 apollo-client
并执行获取客户端。出于多种原因,这对我们有帮助:
- 对于小型查询,性能影响可以忽略不计。
- 如果我们需要为 CMS 层的更改重建页面,这会通过 Next/Gatsby 的检测机制滑动,因此我们可以在不触发巨大的 site-wide 重建的情况下进行全局更改。
那么这到底是什么样子的呢?在组件级别,它看起来就像一个常规的 Apollo-enhanced 组件。我通常使用 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
为下一步使用 Apollo 进行设置
Next.js 团队实际上很好地记录了这一点,这让我觉得我并没有完全破解这个工具的工作方式。你可以在他们的仓库中找到 great examples of using Apollo。
让 Apollo 进入下一个应用程序的步骤:
- 创建一个自定义
useApollo
挂钩来设置与您的数据源的连接(我将我的挂钩放在 Next 层次结构中的/lib/apollo/apolloClient.js
中,但我相信它可以放在其他地方)。
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
}
- 在Next的
/pages/
目录下修改_app.js
。这基本上是 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 获取组件内部的动态数据!太简单了 ;) 哈!
对于 NextJS 中的全局数据获取,我使用 react-query
并且不需要全局状态,因为它可以让您缓存数据。假设您有一个包含类别的博客,并且您希望将类别名称作为下拉菜单放在 navbar
中。在这种情况下,您可以调用 API 从 navbar
组件中获取带有 react-query
的数据并将其缓存。导航栏数据将适用于所有页面。