<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:series="https://publishpress.com/"
	>

<channel>
	<title>Hoang Long La, Author at Tomoshare</title>
	<atom:link href="https://blog.tomosia.com.vn/author/hoanglongla/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.tomosia.com.vn/author/hoanglongla/</link>
	<description>Kênh chia sẻ kiến thức Tomosia Việt Nam</description>
	<lastBuildDate>Thu, 28 Dec 2023 09:26:31 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://blog.tomosia.com.vn/wp-content/uploads/2023/09/cropped-icon-32x32.png</url>
	<title>Hoang Long La, Author at Tomoshare</title>
	<link>https://blog.tomosia.com.vn/author/hoanglongla/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Khởi tạo dự án với Apollo và NextJS SSR</title>
		<link>https://blog.tomosia.com.vn/khoi-tao-du-an-voi-apollo-va-nextjs-ssr/</link>
					<comments>https://blog.tomosia.com.vn/khoi-tao-du-an-voi-apollo-va-nextjs-ssr/#comments</comments>
		
		<dc:creator><![CDATA[Hoang Long La]]></dc:creator>
		<pubDate>Thu, 28 Dec 2023 09:26:30 +0000</pubDate>
				<category><![CDATA[Chưa phân loại]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=2849</guid>

					<description><![CDATA[<p>Gần đây mình có làm việc với 1 dự án sử dụng NextJS và Apollo, mọi việc đang&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/khoi-tao-du-an-voi-apollo-va-nextjs-ssr/">Khởi tạo dự án với Apollo và NextJS SSR</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Gần đây mình có làm việc với 1 dự án sử dụng NextJS và Apollo, mọi việc đang rất yên bình cho đến một ngày khách hàng yêu cầu chuyển đổi từ CSR sang SSR. Kiếp nạn này mình đã trải qua rồi nên cũng tự tin chút cho đến khi nhận ra Apollo (GraphQL) khác với REST API mình đã từng làm.</p>



<blockquote class="wp-block-quote is-style-default has-black-color has-text-color has-link-color has-medium-font-size wp-elements-005da075de4e7366837004f5909fc375 is-layout-flow wp-block-quote-is-layout-flow" style="font-style:normal;font-weight:400">
<p>Apollo:</p>
<cite>Theo tài liệu Apollo cung cấp, phương thức getDataFromTree sẽ tìm kiếm trong page tree của mình các component có sử dụng hook <em>useQuery</em>  sau đó thực hiện truy vấn tất cả các query đó ở server </cite></blockquote>



<p>Nghe có vẻ ổn phết nhưng đội ngũ NextJS nói không. <code>getDataFromTree</code> được đánh giá là anti-pattern đối với NextJS vì <em><code>getDataFromTree</code></em> không được tích hợp vào các phương thức mà NextJS đã cung cấp cho việc fetch dữ liệu như <code>getStaticProps</code> và <code>getServerSideProps</code>. Việc <code>getDataFromTree</code> duyệt page tree một lần nữa để tìm kiếm các useQuery cũng ảnh hưởng đến hiệu suất render trang. Và cuối cùng là mình gặp lỗi tích hợp <code>getDataFromTree</code> vào version của NextJS hiện tại của dự án : D.</p>



<p>Thật may thì cuối cùng NextJS cũng đưa ra một phương án khác đối với Apollo vẫn tận dụng được các phương thức SSR của NextJS đó là tận dụng sức mạnh của Apollo cache.</p>



<h2 id="apollo-cache" class="wp-block-heading">Apollo cache</h2>



<p>Apollo Client sử dụng một hệ thống cache để tối ưu hóa việc quản lý và truy cập dữ liệu trong ứng dụng của bạn. Cache trong Apollo Client là nơi lưu trữ tạm thời kết quả của các truy vấn GraphQL, cho phép ứng dụng của bạn tái sử dụng dữ liệu này mà không cần phải gửi lại truy vấn đến server. Nó giúp giảm thiểu số lượng yêu cầu mạng, giảm độ trễ và cải thiện hiệu suất ứng dụng.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img fetchpriority="high" decoding="async" width="461" height="301" src="http://blog.tomosia.com.vn/wp-content/uploads/2023/12/Untitled-1.png" alt="" class="wp-image-2854" srcset="https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Untitled-1.png 461w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Untitled-1-300x196.png 300w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Untitled-1-380x248.png 380w" sizes="(max-width: 461px) 100vw, 461px" /></figure>
</div>


<p>Ý tưởng của mình sẽ là fetch data thông qua getServerSideProps. Sau khi đã fetch tất cả các dữ liệu FE server sẽ gen ra một file html đầy đủ và trả về thêm dữ liệu từ Apollo thông qua props cho client. Việc trả về thêm Apollo data giúp chúng ta tận dụng được các thuộc tính data, loading và các phương thức như refetch của Apollo client và đồng bộ dữ liệu giữa client và FE server lúc khởi tạo trang thông qua Apollo cache</p>



<h2 id="bat-dau-thoi" class="wp-block-heading">Bắt đầu thôi</h2>



<ul class="wp-block-list">
<li>Khởi đầu với tạo dự án NextJS</li>
</ul>



<pre class="wp-block-code"><code>npx create-next-app</code></pre>



<ul class="wp-block-list">
<li>Cài các package liên quan đến Apollo client. Ở đây mình cài thêm graphql-codegen để tự động gen các hook React từ các document và schema</li>
</ul>



<pre class="wp-block-code"><code>npm i graphql @apollo/client @graphql-codegen/typescript-react-apollo @graphql-codegen/cli</code></pre>



<ul class="wp-block-list">
<li>Cài vài package dùng để xử lí logic và tạo proxy ẩn danh</li>
</ul>



<pre class="wp-block-code"><code>npm i http-proxy deepmerge lodash.isequal @types/lodash.isequal @types/http-proxy</code></pre>



<ul class="wp-block-list">
<li>Tạo file .env để chứ API_URL</li>
</ul>



<pre class="wp-block-code"><code>NEXT_PUBLIC_API_URL=https://beta.pokeapi.co/graphql/v1beta</code></pre>



<ul class="wp-block-list">
<li>Tạo file index.ts trong thư mục src/api để tạo 1 http proxy</li>
</ul>



<pre class="wp-block-code"><code>import httpProxy from 'http-proxy';
import type { NextApiRequest, NextApiResponse } from 'next';

// You can export a config variable from any API route in Next.js.
// We'll use this to disable the bodyParser, otherwise Next.js
// would read and parse the entire request body before we
// can forward the request to the API. By skipping the bodyParser,
// we can just stream all requests through to the actual API.
export const config = {
  api: {
    bodyParser: false,
  },
};

const proxy = httpProxy.createProxyServer();

export default async function handler(req: NextApiRequest, res: NextApiResponse&lt;unknown&gt;) {
  // Return a Promise to let Next.js know when we're done
  // processing the request:
  return new Promise(async (resolve, reject) =&gt; {
    // Rewrite the URL: strip out the leading '/api'.
    // For example, '/api/login' would become '/login'.
    // ️You might want to adjust this depending
    // on the base path of your API.
    req.url = req?.url?.replace(/^\/api/, '');

    // // Handle error
    proxy.once('error', () =&gt; {
      reject();
    });

    proxy.once('proxyRes', () =&gt; {
      resolve(true);
    });

    // Forward the request to the API
    proxy.web(req, res, {
      target: process.env.NEXT_PUBLIC_API_URL,
      autoRewrite: false,
      changeOrigin: true,
      selfHandleResponse: false,
    });
  });
}</code></pre>



<ul class="wp-block-list">
<li>Config file codegen bằng lệnh <code>npx graphql-code-generator init</code> hoặc tạo file codegen.ts ở ngoài cùng thư mục dự án và copy paste đoạn config dưới này</li>
</ul>



<pre class="wp-block-code"><code>import type { CodegenConfig } from '@graphql-codegen/cli';
import { loadEnvConfig } from '@next/env';

loadEnvConfig(process.cwd());

const config: CodegenConfig = {
  overwrite: true,
  schema: process.env.NEXT_PUBLIC_API_URL,
  documents: &#91;'./src/graphql/**/*.{gql,graphql}'],
  ignoreNoDocuments: true, // for better experience with the watcher
  generates: {
    'src/__generated__/graphql.tsx': {
      plugins: &#91;'typescript', 'typescript-operations', 'typescript-react-apollo'],
    },
  },
};

export default config;</code></pre>



<ul class="wp-block-list">
<li>Định nghĩa một đối tượng Apollo client và hàm addApolloState ở file src/lib/apolloClient.ts</li>
</ul>



<pre class="wp-block-code"><code>import {
  ApolloClient,
  InMemoryCache,
  type NormalizedCacheObject,
} from '@apollo/client';
import merge from 'deepmerge';
import isEqual from 'lodash.isequal';


const COUNTRIES_API = 'https://countries.trevorblades.com';

export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__';

let apolloClient: ApolloClient&lt;NormalizedCacheObject&gt; | null;

function createApolloClient() {   
  return new ApolloClient({
    ssrMode: typeof window === 'undefined', 
    uri: typeof window === 'undefined' ? `${process.env.NEXT_PUBLIC_API_URL}` : '/api',
    cache: new InMemoryCache(),
  });
}

export function initApolloClient(initialState?: any) {
  const _apolloClient = apolloClient ?? createApolloClient();

  if (initialState) {
    const existingCache = _apolloClient.cache.extract();

    const data = merge(initialState, existingCache, {
      arrayMerge: (destinationArray, sourceArray) =&gt; &#91;
        ...sourceArray,
        ...destinationArray.filter((d) =&gt;
          sourceArray.every((s) =&gt; !isEqual(d, s))
        ),
      ],
    });
    _apolloClient.cache.restore(data);
  }

  if (typeof window === 'undefined') {
    return _apolloClient;
  }

  if (!apolloClient) {
    apolloClient = _apolloClient;
  }

  return _apolloClient;
}

export function addApolloState(
  client: ApolloClient&lt;NormalizedCacheObject&gt;,
  pageProps: any
) {
  if (pageProps?.props) {
    pageProps.props&#91;APOLLO_STATE_PROP_NAME] = client.cache.extract();
  }

  return pageProps;
}</code></pre>



<p>Hàm <code>createApolloClient</code> trả về một đối tượng Apollo client: tham số uri định nghĩa API_URL. Khi ở server side call API sẽ gọi trực tiếp đến API_URL của BE, còn phía client side sẽ gọi thông qua proxy mình đã tạo ở trên để tăng tính bảo mật cho API_URL </p>



<p>Hàm <code>initApolloClient</code> đảm bảo trong suốt phiên làm việc của web chỉ có duy nhất 1 đối tượng Apollo client được khởi tạo</p>



<p>Hàm <code>addApolloState</code> sẽ đẩy data đã được fetch từ server side vào 1 prop có tên là __APOLLO_STATE__ sau đó sẽ được gửi đến client side thông qua pageProps là giá trị mặc định trả về của hàm <code>getServerSideProps</code></p>



<ul class="wp-block-list">
<li>Tiếp theo ta sẽ tạo một hook useApollo ở file src/hooks/useApollo.ts. Hook này sẽ theo dõi sự thay đổi của prop __APOLLO_STATE__ và sẽ quyết định tạo mới một đối tượng Apollo client nếu chưa có hoặc ngược lại sẽ trả về đối tượng Apollo client đã tồn tại</li>
</ul>



<pre class="wp-block-code"><code>import { APOLLO_STATE_PROP_NAME, initApolloClient } from '@/lib/apolloClient';
import { useMemo } from 'react';

function useApollo(pageProps: any) {
  const state = pageProps&#91;APOLLO_STATE_PROP_NAME];
  const client = useMemo(() =&gt; initApolloClient(state), &#91;state]);

  return client;
}

export default useApollo;
</code></pre>



<p>Đến đây mọi thứ đã gần như hoàn thiện rồi việc cuối cùng là tạo một query ví dụ và kiểm tra xem SSR có hoạt động đúng như mong đợi của chúng ta hay không thôi</p>



<h2 id="thu-nghiem" class="wp-block-heading">Thử nghiệm</h2>



<ul class="wp-block-list">
<li>Tạo một query document ở file src/graphql/queries/getPokemons.query.gql. Ở đây mình muốn lấy danh sách tất cả tên các Pokemon nên là câu query sẽ dạng như thế này:</li>
</ul>



<pre class="wp-block-code"><code>query getPokemons {
  pokemon_v2_pokemonspecies {
    id
    name
  }
}</code></pre>



<ul class="wp-block-list">
<li>Chạy lệnh <code>npm run codegen</code> để graphql-codegen đọc file schema và các file document để tạo ra các đối tượng document và hook và NextJS có thể đọc được nhé</li>



<li>File src/pages/index.tsx mình sửa lại để call hook <code>useGetPokemonsQuery</code> và render data nhé.</li>
</ul>



<pre class="wp-block-code"><code>import { GetPokemonsDocument, useGetPokemonsQuery } from '@/__generated__/graphql'
import { addApolloState, initApolloClient } from '@/lib/apolloClient'

export default function Home() {
  const { data } = useGetPokemonsQuery()

  return (
    &lt;main
      className={`flex min-h-screen flex-col items-center justify-between p-24`}
    &gt;
      &lt;div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm flex flex-col"&gt;
        {data?.pokemon_v2_pokemonspecies.map((pokemon) =&gt; (
          &lt;div key={pokemon.id}&gt;{pokemon.name}&lt;/div&gt;
        ))}
      &lt;/div&gt;
    &lt;/main&gt;
  )
}</code></pre>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="578" src="http://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.50.38-PM-1024x578.png" alt="" class="wp-image-2860" srcset="https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.50.38-PM-1024x578.png 1024w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.50.38-PM-300x169.png 300w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.50.38-PM-768x434.png 768w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.50.38-PM-1536x867.png 1536w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.50.38-PM-2048x1156.png 2048w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.50.38-PM-380x215.png 380w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.50.38-PM-800x452.png 800w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.50.38-PM-1160x655.png 1160w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.50.38-PM.png 2880w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="580" src="http://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.51.09-PM-1024x580.png" alt="" class="wp-image-2861" srcset="https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.51.09-PM-1024x580.png 1024w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.51.09-PM-300x170.png 300w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.51.09-PM-768x435.png 768w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.51.09-PM-1536x869.png 1536w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.51.09-PM-2048x1159.png 2048w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.51.09-PM-380x215.png 380w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.51.09-PM-800x453.png 800w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.51.09-PM-1160x657.png 1160w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.51.09-PM.png 2880w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Code thì đã chạy ổn đã fetch được data từ Graphql nhưng vẫn chưa đúng với mục tiêu của blog đưa ra là trang này phải chạy ở mode SSR. Vậy thì thêm tiếp dòng code dưới ở src/pages/index.tsx nhé.</p>



<pre class="wp-block-code"><code>export const getServerSideProps = async () =&gt; {
  const client = initApolloClient();
  await client.query({ query: GetPokemonsDocument});

  return addApolloState(client, {
    props: {},
  });
};</code></pre>



<p>Đây là kết quả cuối cùng. Ở tab network ta sẽ không còn thấy request /api với payload getPokemons nữa và khi ta bật tab view page source nên sẽ thấy file html gen đầy đủ các thẻ div chứa các tên pokemon</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="581" src="http://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.56.52-PM-1024x581.png" alt="" class="wp-image-2862" srcset="https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.56.52-PM-1024x581.png 1024w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.56.52-PM-300x170.png 300w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.56.52-PM-768x436.png 768w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.56.52-PM-1536x871.png 1536w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.56.52-PM-2048x1162.png 2048w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.56.52-PM-380x216.png 380w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.56.52-PM-800x454.png 800w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.56.52-PM-1160x658.png 1160w, https://blog.tomosia.com.vn/wp-content/uploads/2023/12/Screen-Shot-2023-12-24-at-6.56.52-PM.png 2880w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>Khi mọi người đọc code sẽ thấy query được gọi 2 lần. </p>



<p>1 lần ở getServerSideProps: <code>await client.query({ query: GetPokemonsDocument}); </code><br>Và 1 lần nữa ở function component: <code>const { data } = useGetPokemonsQuery()</code>. Sẽ tự hỏi liệu việc gọi lại này có phải sẽ tạo 2 request đến BE không thì câu trả lời là không nhé. Vì gọi lại này nhằm mục đích tận dụng các thuộc tính data, các phương thức refresh do Apollo cung cấp, dễ quản lý dữ liệu ở function component hơn và data lấy từ function component là data được trả về thông qua pageProps mình đã giải thích ở trên nên không ảnh hưởng đến hiệu suất của trang web.<br></p>



<p>Mn có thể tham khảo code đầy đủ hơn ở: <a href="https://github.com/long-la-tomosia/nextjs-ssr-apollo/tree/main">https://github.com/long-la-tomosia/nextjs-ssr-apollo/tree/main</a></p>



<p>Bài viết dựa trên việc nghiên cứu của 1 cá nhân nên có thể sẽ có 1 vài sai sót mong mọi người góp ý và chia sẻ để hoàn thiện hơn. Cảm ơn mọi người đã theo dõi bài viết : D</p>
<p>The post <a href="https://blog.tomosia.com.vn/khoi-tao-du-an-voi-apollo-va-nextjs-ssr/">Khởi tạo dự án với Apollo và NextJS SSR</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/khoi-tao-du-an-voi-apollo-va-nextjs-ssr/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
	</channel>
</rss>
