222 lines
6.4 KiB
Markdown
222 lines
6.4 KiB
Markdown
All pages are **server components** by default. Only pages with `'use-client'` on top are **client components**.
|
|
|
|
**Server components fetch data on the server and pre-render the HTML.** For example, in the following page:
|
|
|
|
```tsx
|
|
import LikeButton from '@/app/ui/like-button'
|
|
import { getPost } from '@/lib/data'
|
|
|
|
export default async function Page({ params }: { params: { id: string } }) {
|
|
const post = await getPost(params.id)
|
|
|
|
return (
|
|
<div>
|
|
<main>
|
|
<h1>{post.title}</h1>
|
|
{/* ... */}
|
|
<LikeButton likes={post.likes} />
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
'use client'
|
|
|
|
import { useState } from 'react'
|
|
|
|
export default function LikeButton({ likes }: { likes: number }) {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
The `getPost` function is run server-side to get the data and render the whole `div` tag except for the `LikeButton` which is rendered client-side.
|
|
|
|
Using any hooks e.g. `useQuery` or `useMutation` requires us to do so inside a `client component`.
|
|
|
|
We can also have `Next.js` fetches data and pre-renders HTML **at build time and on periodical revalidations** using [**Incremental Static Regeneration (ISR)**](https://nextjs.org/docs/app/guides/incremental-static-regeneration) as follows:
|
|
|
|
```tsx
|
|
interface Post {
|
|
id: string
|
|
title: string
|
|
content: string
|
|
}
|
|
|
|
// Next.js will invalidate the cache when a
|
|
// request comes in, at most once every 60 seconds.
|
|
export const revalidate = 60
|
|
|
|
// For each `post` in the array returned by `generateStaticParams`,
|
|
// we will pre-render and cache a corresponding page at build time and on later revalidations.
|
|
// If a request comes in for a path that hasn't been generated,
|
|
// i.e. if there is a request for a post not retrieved when `generateStaticParams` was last run,
|
|
// Next.js will server-render the page on-demand.
|
|
export const dynamicParams = true // or false, to 404 on unknown paths
|
|
|
|
export async function generateStaticParams() {
|
|
const posts: Post[] = await fetch('https://api.vercel.app/blog').then((res) =>
|
|
res.json()
|
|
)
|
|
return posts.map((post) => ({
|
|
id: String(post.id),
|
|
}))
|
|
}
|
|
|
|
export default async function Page({
|
|
params,
|
|
}: {
|
|
params: Promise<{ id: string }>
|
|
}) {
|
|
const { id } = await params
|
|
const post: Post = await fetch(`https://api.vercel.app/blog/${id}`).then(
|
|
(res) => res.json()
|
|
)
|
|
return (
|
|
<main>
|
|
<h1>{post.title}</h1>
|
|
<p>{post.content}</p>
|
|
</main>
|
|
)
|
|
}
|
|
```
|
|
|
|
SSR with `@apollo/client`:
|
|
|
|
```tsx
|
|
import { Wrapper } from '@/components'
|
|
import { SESSION_COOKIE_NAME } from '@/constants'
|
|
import { MeDocument, PostDocument } from '@/generated/graphql/graphql'
|
|
import { createApolloClient } from '@/lib'
|
|
import { Heading, Text } from '@chakra-ui/react'
|
|
import { cookies } from 'next/headers'
|
|
import { ClientSection } from './ClientSection'
|
|
|
|
interface Props {
|
|
params: {
|
|
id: string
|
|
}
|
|
}
|
|
|
|
const PostPage: React.FC<Props> = async ({ params: { id } }) => {
|
|
const cookieStore = cookies()
|
|
const cookie = cookieStore.get(SESSION_COOKIE_NAME)?.value
|
|
const apollo = await createApolloClient(cookie)
|
|
const { data } = await apollo.query({ query: PostDocument, variables: { id } })
|
|
const { data: meData } = await apollo.query({ query: MeDocument })
|
|
|
|
return (
|
|
<Wrapper>
|
|
{
|
|
data?.post ?
|
|
<>
|
|
<Heading as='h3' size='md'>{data.post.title}</Heading>
|
|
<Text>Posted by {data.post.author.username}</Text>
|
|
<Text mt={4}>{data.post.content}</Text>
|
|
{
|
|
data?.post?.authorID == meData?.me?.id &&
|
|
<ClientSection data={data} />
|
|
}
|
|
</>
|
|
:
|
|
<>Post not found!</>
|
|
}
|
|
</Wrapper>
|
|
)
|
|
}
|
|
export default PostPage
|
|
```
|
|
|
|
where `createApolloClient` is:
|
|
|
|
```tsx
|
|
'use server'
|
|
import { SESSION_COOKIE_NAME } from '@/constants'
|
|
import { from, HttpLink, NormalizedCacheObject } from '@apollo/client'
|
|
import { onError } from '@apollo/client/link/error'
|
|
import { ApolloClient, InMemoryCache, registerApolloClient } from '@apollo/experimental-nextjs-app-support'
|
|
|
|
|
|
const errorLink = onError(({ graphQLErrors, networkError }) => {
|
|
graphQLErrors?.forEach(({ message, locations, path }) => {
|
|
console.log(
|
|
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
|
|
)
|
|
})
|
|
if (networkError) console.error(`[Network error]: ${networkError}`)
|
|
})
|
|
|
|
|
|
// This is the server-side client
|
|
export const createApolloClient = async (cookie?: string) => registerApolloClient<ApolloClient<NormalizedCacheObject>>(() => {
|
|
const httpLink = new HttpLink({
|
|
uri: process.env.NEXT_PUBLIC_BACKEND_URI,
|
|
credentials: 'include',
|
|
headers: {
|
|
cookie: cookie ? `${SESSION_COOKIE_NAME}=${cookie}` : ''
|
|
}
|
|
})
|
|
return new ApolloClient({
|
|
ssrMode: true,
|
|
link: from([errorLink, httpLink]),
|
|
cache: new InMemoryCache()
|
|
})
|
|
})
|
|
```
|
|
|
|
If we use `react-query`, we can combine the initial server-side data fetching and subsequent `useQuery` calls as follows:
|
|
|
|
```tsx
|
|
// app/posts/page.tsx
|
|
import {
|
|
dehydrate,
|
|
HydrationBoundary,
|
|
QueryClient,
|
|
} from '@tanstack/react-query'
|
|
import Posts from './posts'
|
|
|
|
export default async function PostsPage() {
|
|
const queryClient = new QueryClient()
|
|
|
|
|
|
await queryClient.prefetchQuery({
|
|
queryKey: ['posts'],
|
|
queryFn: getPosts,
|
|
})
|
|
|
|
return (
|
|
// Hydration is when React converts the pre-rendered HTML from the server into a fully interactive application by attaching event handlers.
|
|
// Dehydration, on the other hand, is when an interactive JavaScript object is converted into HTML.
|
|
// Here we are converting the queryClient which contains data fetched server-side into HTML and passing that HTML into HydrationBoundary, which is a Client component.
|
|
// When HydrationBoundary is rendered client-side, the HTML will be hydrated, i.e. the data fetched server-side will be integrated with useQuery seamlessly.
|
|
<HydrationBoundary state={dehydrate(queryClient)}>
|
|
<Posts />
|
|
</HydrationBoundary>
|
|
)
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
// app/posts/posts.tsx
|
|
'use client'
|
|
|
|
export default function Posts() {
|
|
// This useQuery could just as well happen in some deeper
|
|
// child to <Posts>, data will be available immediately either way
|
|
const { data } = useQuery({
|
|
queryKey: ['posts'],
|
|
queryFn: () => getPosts(),
|
|
})
|
|
|
|
// This query was not prefetched on the server and will not start
|
|
// fetching until on the client, both patterns are fine to mix.
|
|
const { data: commentsData } = useQuery({
|
|
queryKey: ['posts-comments'],
|
|
queryFn: getComments,
|
|
})
|
|
|
|
// ...
|
|
}
|
|
```
|