Next.jsのComponentでgetStaticPropsをしたい
Next.jsをSSG目当てに使用している場合、あるcomponentのpropsについて、プリレンダリング時にAPIからfetchしたデータなどを使いたいという場合があると思います。
Next.jsのNextPage
では、getStaticProps
を用いることで、ビルド時にgetStaticProps
から返される props
を使ってプリレンダリングしてくれます。
例えば、上のリンクカードコンポーネントはリンク先がAccess-Control-Allow-Origin: *
になっている保障も無く、またビルド時に内容が決定できるので、getStaticProps
でビルド時にプリレンダリングしてあげたいところです。
しかし、上のリンクカードにある通り、getStaticProps
が使えるのはNextPage
のみです。
調べてみると、やはり同じようなことについて書いている先駆者さんがいました。
この記事では、next-cmsでどのようにComponent-level static propsをしているかについて解説されています。
詳しくは以上の記事を読んで頂くこととして、このブログで使用しているOGPを取得してリンクカードにするものを例に簡単に解説します。
まず、createContext
を用いてOGPContext
を作成し、コンポーネントをそのProviderでラップするwithOPGPage
を定義します。
import { createContext } from 'react'
export const OGPContext = createContext({})
export const withOGPPage = (Component) =>
function inject(props) {
if (props.__next_ssg_error) {
// render error page
return <h1>{props.__next_ssg_error} Error</h1>
}
return (
<OGPContext.Provider value={props.__next_ssg_data || {}}>
<Component {...props} />
</OGPContext.Provider>
)
}
続いて、以下のようなコンポーネントを作成します。
このコンポーネントは、プリレンダリング時とレンダリングで動作が変わります。
レンダリング時の動作は、後述するとして、プリレンダリング時はグローバルな配列global.__next_ssg_requests
にendpoint
を追加し、何も返さないコンポーネントとなっています。
import { useContext } from 'react'
import { OGPContext } from '../withOGPPage'
export default function OGP({ endpoint, children }) {
const data = useContext(OGPContext)
// プリレンダリング時はここがtrueにならない
if (typeof data[endpoint] !== 'undefined') {
if (typeof children === 'function') {
return children(data[endpoint])
}
return data[endpoint]
}
const IS_SSG =
typeof window === 'undefined' &&
typeof global !== 'undefined' &&
global.__next_ssg_requests
// SSGの場合、グローバルな配列にpushする
if (IS_SSG) {
global.__next_ssg_requests.push(endpoint)
}
return null
}
このOGP
コンポーネントは以下のようにコンポーネントをラップする形で使われます。
例として、以下のコンポーネントでは、propsで渡されたhref
をプリレンダリング時にglobal.__next_ssg_requests
に追加します。
export const LinkCard: React.FC<LinkCardProps> = ({ href }) => {
return (
<OGP endpoint={href}>
{(data) => {
return (
<div>
{data['og:title']}
</div>
)
}}
</OGP>
)
}
このコンポーネントが用いられるページは以下のように、withOGPPage
でラップしてあげます。
そして、このページのgetStaticProps
中では、実際にfetchしてOGPを取得するgetOGPStaticProps
を実行します。
const Hoge: NextPage<HogeProps> = withOGPPage(({ href, fuga }) => {
return (
<LinkCard href={href} />
)
})
export default Hoge
export const getStaticProps: GetStaticProps = async (ctx) => {
const fuga = "fuga"
const props = await getOGPStaticProps(Hoge, {
fuga: fuga,
})
return { props }
}
getOGPStaticProps
は以下のようになっています。
やっていることは、グローバルな配列global.__next_ssg_requests
に格納されているendpoint
を1つづつ取り出してfetchした結果を連想配列__next_ssg_data
にendpoint
をキーとして格納し、propsに追加して返しています。
export const getOGPStaticProps = async (Page, extraProps) => {
const ReactDOMServer = require('react-dom/server')
const fetch = require('node-fetch')
const JSDOM = require('jsdom')
const props = { __next_ssg_data: {}, ...extraProps }
const pendingPromises = {}
while (true) {
global.__next_ssg_requests = []
ReactDOMServer.renderToStaticMarkup(<Page {...props} />)
if (!global.__next_ssg_requests.length) break
const endpoints: string[] = Array.from(new Set(global.__next_ssg_requests))
try {
await Promise.race(
endpoints.map((endpoint) => {
if (!pendingPromises[endpoint]) {
pendingPromises[endpoint] = pendingPromises[endpoint] = fetch(
endpoint,
{
mode: 'cors',
credentials: 'omit',
}
)
.then((res) => {
if (!res.ok) {
delete pendingPromises[endpoint]
throw res.status
}
return res.text()
})
.then((text) => {
const jsdom = new JSDOM.JSDOM()
const parser = new jsdom.window.DOMParser()
const el = parser.parseFromString(text, 'text/html')
const headEls = el.head.children
const data = {}
Array.from(headEls).map((v: any) => {
const prop = v.getAttribute('property')
const name = v.getAttribute('name')
if (!prop && !name) {
return
}
data[prop ?? name] = v.getAttribute('content')
})
props.__next_ssg_data[endpoint] = data
delete pendingPromises[endpoint]
})
}
return pendingPromises[endpoint]
})
)
} catch (err) {
return { __next_ssg_error: err }
}
}
return props
}
ここまで、プリレンダリング時の動作をまとめると、OGP
コンポーネントのendpoint
が集約され、getOGPStaticProps
で格納されているendpoint
のデータをfetchし、ページのprops
の__next_ssg_data
にデータが格納されます。
先述した通り、OGP
コンポーネントはプリレンダリング時とレンダリング時で動作が変わります。
レンダリング時のOGP
コンポーネントでは、プリレンダリング時はdata
が空でしたが、withOPGPage
によって、ページのprops
の__next_ssg_data
がOGPContext
に格納されているので、はじめのifの条件式がtrue
となり、取得したdata
に基づき、children
がレンダリングされます。
import { useContext } from 'react'
import { OGPContext } from '../withOGPPage'
export default function OGP({ endpoint, children }) {
const data = useContext(OGPContext)
// ここを通るようになる
if (typeof data[endpoint] !== 'undefined') {
if (typeof children === 'function') {
return children(data[endpoint])
}
return data[endpoint]
}
const IS_SSG =
typeof window === 'undefined' &&
typeof global !== 'undefined' &&
global.__next_ssg_requests
if (IS_SSG) {
global.__next_ssg_requests.push(endpoint)
}
return null
}
以上のように、React Contextを巧みにつかうことで、Component-level static propsができるようになります。
プリレンダリング・レンダリングの2つのレンダリングとReact Contextが絡み合っているため、複雑ですが、一度動作を追ってみると理解しやすいと思います。
自分のブログの全体のコードはあまり人に見せられるものではないので、今回はスニペットだけ書きました。
途中で言及した以下のリポジトリでは、説明した仕組みを含めたコード全体が確認できるので、参考になると思います。