Migrating from Gatsby to Next.js

It's quite a bit of work since it's 2 different frameworks. The codebase on gatsby was under-maintained, so it was a bit painful to upgrade the packages.

I left out some of the features to be worked/not worked on next:

  1. Dark mode switch: it never work in my gatsby setup.
  2. Just javascript, less prone to change: less thing to maintain.
  3. Removed styled-components: less useful.

The Migration

App Router

Since the latest version encourage App Router, so I went for it as well. However, the documentation is pretty much mixed up with old Page Router, so it made it a bit difficult to find which part works for which part.

# changes from pages/ to
app/[slug]/
  page.js

app/components/
  [component-name].js

SSG (Static site generation)

I want to keep SSG of Gatsby in Next.js, so the design/steps I took should follow the direction as well.

Since I want to prerender my photos from Sanity.io, so using it along with Server Components is a must.

Server Components

React Server Components was introducted to enable server side rendering. By default Next.js will render the components as server components, unless otherwise stated e.g., "use client" on top of the files.

This gives many warnings/errors when I first moved the components over. so basically, any components that uses useState, useLayout, Context will be flagged.

2 caveats

  1. I cannot just mix and match server components and client components
  2. I cannot pass non-serializable props from server componnets and client components

For example:

  1. Counter example here when server components cannot be imported and used in client components.
'use client'
export default function ClientComponent({children}) {
  const [count, setCount] = useState(0)
 
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <ServerComponent />
    </>
  )
}
  1. Good example here, when how Server Components can be rendered inside child components with the use of composition.
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )
}

The idea is to encourage a new structure/rule of thumbs where server components are kept in the upstream, client components in the downstream.

Some refactoring was done:

  1. Differentiate and isolate the components from top down, "delay" and start making server components until it cannot be done, then set child components as client components (e.g., "use client"). Basically I tried first not use any useState, useEfect, useLayoutEffect in the server components, refactor them and use them later in the client components.

  2. Problem came when it creates a gap between client components when we cannot just pass the e..g.,props.setPreviewItem to the other component. That's when React Context API came in to help. It looks like this

"use client";

export const ClientComponent = ({ children }) => {
  return (
    <section>
      {/* children is server components that consist of other client components */}
      <PreviewContext.Provider>{children}</PreviewContext.Provider>
    </section>
}

Sanity.io Integration

I need to convert Gatsby query into GROQ language for Sanity.io, then use this in the server components, no more getStaticProps. e.g., const albums = await getAlbums().

// Instantiate Sanity.io client
const client = createClient({
  token: process.env.NEXT_PUBLIC_SANITY_TOKEN,
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET,
  useCdn: true, // `false` if you want to ensure fresh data
});
// getAlbums by querying client with GROQ
export async function getAlbums() {
  const query = `*[_type == "album"]{
    _id,
    title,
    photos[]{
      "assetId": asset->_id,
      "originalFilename": asset->originalFilename,
      "width": asset->metadata.dimensions.width,
      "height": asset->metadata.dimensions.height
    }
  }`;
  try {
    const data = await client.fetch(query, { cache: "force-cache" });
    return data;
  } catch (err) {
    return [];
  }
}
// Extract url for use in img.src 
import imageUrlBuilder from "@sanity/image-url";
const builder = imageUrlBuilder(client);

// Usage: urlFor(assetId).width(width).height(height).url()
export function urlFor(source) {
  return builder.image(source);
}

Build Output

// next.config.js
const nextConfig = {
  output: "export",    // export as static files
  images: {
    unoptimized: true, // "output: export" do not support optimized image
    remotePatterns: [  // allowing remote image urls
      {
        protocol: "https",
        hostname: "cdn.sanity.io",
        port: "",
        pathname: `/images/**`, 
      },
    ],
  },
};

References

  1. Rendering: https://nextjs.org/docs/app/building-your-application/rendering
  2. GROQ cheatsheet: https://www.sanity.io/docs/query-cheat-sheet

Created 2023-12-09T20:49:38+08:00 · Edit