---
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
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,
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user