NextJS Note

NextJS Note

NextJs notes

This is a note for Next.js knowledge

File Base system

useRouter

const router = useRouter(); console.log(router.pathname); console.log(router.query);

catch-all routes

Navigate with Link component

the way with React"

const ClientPage = () => { const clients = [ { id: "max", name: "max" }, { id: "ricky", name: "ricky" }, ]; return ( <div> <h1>ClientPage</h1> <ul> {clients.map((client) => { return ( <li key={client.id}> <Link href={`/clients/${client.id}`}> {client.name} </Link> </li> ); })} </ul> </div> ); }; export default ClientPage;

different way to set the dynamic link comoponent:

const ClientPage = () => { const clients = [ { id: "max", name: "max" }, { id: "ricky", name: "ricky" }, ]; return ( <div> <h1>ClientPage</h1> <ul> {clients.map((client) => { return ( <li key={client.id}> <Link href={{ pathname: "/clients/[id]", query: { id: client.id }, }} > {client.name} </Link> </li> ); })} </ul> </div> ); };

Navigate Programmatically

router.push("/clients/max"); router.push("/clients/max"); //can't go back router.push({ pathname: "/clients/[id]/[clientprojectId]", query: { id: "max", clientprojectId: "1" }, });

_app.js:

  • this is the root componet where all the page components render in, you could add the layout component here

useRouter access the url data on the second time render

Data Fetching

renderings

By default, NextJS pre-renders all pages on server side.

  • Pre-rendering only affects the initial load(first time the website or refresh the website), so just the initial page which we visited is pre-rendered, so only the pages file coudl use it

Static Generation (general in build time, before deployment, you could also set re-run using revalidate so you do not need to re-build it all the time)

  • You could run any code that could normally run on the server side only, not any client side, no access to any client API. Codes inside will NOT be included in the bundle sent back to the client. The code will never end up in client side

  • it run the getStaticProps first, then your component code, must always return an object with props key

getStaticProps

export async function getStaticProps() { return { props: {} }; }
  • Incremental Static Generation: static-generation-1

    export async function getStaticProps() { const fs = require("fs").promises; const filePath = path.join(process.cwd(), "data", "dummy-backend.json"); const jsonData = await fs.readFile(filePath); const data = JSON.parse(jsonData); return { props: { product: data.products, }, revalidate: 10, }; }

    when the user load(like refresh the page) the page, the server will check if the time of pages last time got generated is more than 10 second ago, if yes, re-run the getStaticProps function

  • the notFound and redirect:

    export async function getStaticProps() { const fs = require("fs").promises; const filePath = path.join(process.cwd(), "data", "dummy-backend.json"); const jsonData = await fs.readFile(filePath); const data = JSON.parse(jsonData); if (!data) { return { redirect: { destination: "https://google.com" } }; } if (!data.products) { return { notFound: true }; } return { props: { product: data.products, }, revalidate: 120, }; }
  • context to get hold of the url param values in the path

getStaticPaths

  • getStaticPaths - for dynamic page, the default behavior is not to pre-generate the page since nextjs will not know what dynamic value need to be support - Pre-Generated Paths (Routes) static-generation-2 - getStaticPaths is to tell nextjs which dynamic path should be generated

    export const getStaticProps = async (context) => { const { params } = context; const productId = params.pid; const fs = require("fs").promises; const filePath = path.join(process.cwd(), "data", "dummy-backend.json"); const jsonData = await fs.readFile(filePath); const data = JSON.parse(jsonData); if (!data) { return { redirect: { destination: "https://google.com" } }; } if (!data.products) { return { notFound: true }; } return { props: { product: data.products.find( (product) => product.id === productId ), }, }; }; export const getStaticPaths = async () => { return { paths: [{ params: { pid: "p1" } }, { params: { pid: "p2" } }], fallback: false, }; };

    getStaticProps will get called two times since there are two elements in the paths array

  • getStaticPaths and Link Prefetching:

    • For the links that are going to the dynamice generated page, it will use the pre-generated json file that contains the props data for that dynamice generated page to render a new page, so it will not make a server request, just render the page like regular react. (pre-generated json file, run build to see this in action)
  • fallback: fallback: true is useful if your app has a very large number of static pages that depend on data (such as a very large e-commerce site). If you want to pre-render all product pages, the builds would take a very long time.

    Instead, you may statically generate a small subset of pages and use fallback: true for the rest. When someone requests a page that is not generated yet, the user will see the page with a loading indicator or skeleton component. (Refer to the nextjs docs)

    • when fallback set to true, when you land on the dynamice page by entering the url, it acts like the standard React client side rendering, like using useEffect and setState, so you need to add the logic when the page is still generating on server

    • when fallback is set to true, we could also set the not found page in the case that the user enter a url with an invalid url param, like a non-exist product id.

    export const getStaticProps = async (context) => { const { params } = context; const productId = params.pid; const data = await getData(); if (!data) { return { redirect: { destination: "https://google.com" } }; } const product = data.products.find( (product) => product.id === productId ); if (!product) { return { notFound: true }; } return { props: { product: product, }, }; }; export const getStaticPaths = async () => { const { products } = await getData(); const ids = products.map((product) => product.id); const pathsWithParams = ids.map((id) => ({ params: { pid: id, }, })); return { paths: pathsWithParams, fallback: true, }; };

Server-side rendering (runs in server after deployments)

  • static generation make the request in build time, so it does not have access for the actual incoming user request

  • server-side rendering is re-executed for every incoming request other than the revalidate in staticProps that only re-executed after a set period of time

  • both SSR and Static generation get props for the component

    server-side-rendering

    import React from "react"; const UserIdPage = (props) => { return <div>{props.id}</div>; }; export default UserIdPage; export const getServerSideProps = async (context) => { const { params, req, res } = context; return { props: { id: "user-id" + params.uid }, }; };

Client Side Rendering

client-side

import React, { useEffect, useState } from "react"; const LastSale = () => { const [sales, setSales] = useState(); const [isLoading, setIsLoading] = useState(true); useEffect(() => { fetch( "https://nextjs-course-e7f0b-default-rtdb.firebaseio.com/sales.json" ) .then((response) => response.json()) .then((data) => { const transformedSales = []; for (const key in data) { transformedSales.push({ id: key, username: data[key].username, volume: data[key].volume, }); } setSales(transformedSales); setIsLoading(false); }); }, []); if (isLoading) { return <p>Loading...</p>; } return ( <ul> {sales.map((sale) => ( <li key={sale.id}> {sale.username} - {sale.volume} </li> ))} </ul> ); }; export default LastSale;

useSWR

  • this hook will bundle multiple requests to the same URL, which are sent in a certain time frame into one request to avoid sending dozens of small requests
import React, { useEffect, useState } from "react"; import useSWR from "swr"; const fetcher = (url) => fetch(url).then((response) => response.json()); const LastSale = () => { const [sales, setSales] = useState([]); // const [isLoading, setIsLoading] = useState(true); const { data, error, isLoading } = useSWR( "https://nextjs-course-e7f0b-default-rtdb.firebaseio.com/sales.json", fetcher ); useEffect(() => { if (data) { const transformedSales = []; for (const key in data) { transformedSales.push({ id: key, username: data[key].username, volume: data[key].volume, }); } setSales(transformedSales); } }, [data]); // useEffect(() => { // console.log(data); // if (data) { // setSales(data); // } // }, [data]); // useEffect(() => { // fetch( // "https://nextjs-course-e7f0b-default-rtdb.firebaseio.com/sales.json" // ) // .then((response) => response.json()) // .then((data) => { // const transformedSales = []; // for (const key in data) { // transformedSales.push({ // id: key, // username: data[key].username, // volume: data[key].volume, // }); // } // console.log(transformedSales); // setSales(transformedSales); // setIsLoading(false); // }); // }, []); if (error) { return <p>There is an error</p>; } if (isLoading || !data || !sales) { return <p>Loading...</p>; } return ( <ul> {sales.map((sale) => ( <li key={sale.id}> {sale.username} - {sale.volume} </li> ))} </ul> ); }; export default LastSale;

Combining Pre-fetching with Client-side fetching

import React, { useEffect, useState } from "react"; import useSWR from "swr"; const fetcher = (url) => fetch(url).then((response) => response.json()); const LastSale = (props) => { const [sales, setSales] = useState(props.sales); // const [isLoading, setIsLoading] = useState(true); const { data, error, isLoading } = useSWR( "https://nextjs-course-e7f0b-default-rtdb.firebaseio.com/sales.json", fetcher ); useEffect(() => { if (data) { const transformedSales = []; for (const key in data) { transformedSales.push({ id: key, username: data[key].username, volume: data[key].volume, }); } setSales(transformedSales); } }, [data]); if (error) { return <p>There is an error</p>; } return ( <ul> {sales.map((sale) => ( <li key={sale.id}> {sale.username} - {sale.volume} </li> ))} </ul> ); }; export const getStaticProps = async () => { const data = await fetch( "https://nextjs-course-e7f0b-default-rtdb.firebaseio.com/sales.json" ).then((response) => response.json()); const transformedSales = []; for (const key in data) { transformedSales.push({ id: key, username: data[key].username, volume: data[key].volume, }); } return { props: { sales: transformedSales }, revalidate: 1000, }; }; export default LastSale;
Static GenerationServer Side RenderingClient Side Rendering
SEOYesYesNo
User-SpecifyNoYesYes
run in build timeYesNoNo
Update frequentlyNorender on server per requestYes

Optimizing NextJS Apps

Meta data

  • next/head With this hook, you can add the <Head></Head> to anywhere, nextjs will inject it to the right place in the head section of the html

Dynamic head content

  • by default, nextjs will initial render the page on server, not runing the useEffect

_app.js:

root component of your website, you could add universal meta data there like: head

<Layout> <Head> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> </Head> <Component {...pageProps} /> </Layout> );
  • Merge <Head> and solve the conflict with the latest tag

_document.js

You can imagine app JS as the root component

inside of the body section of your HTML document.

Document JS allows you to customize

the entire HTML document.

So all the elements that make up an HTML document,

if you need to do that,

you can add to the _documented JS file.

And then you need to add a special component in there,

a class-based component, as it turns out,

which you could name my document,

and it has to be a class-based component

because it must extend some component offered

and provided by next JS

Optimize for images

  • next/image

  • You still override width and height, as you set it here with your CSS styles. So if you give the image a hard-coded width and height, those CSS styles still kick in. The width and height, you set here only determine the image size that will be fetched in the end. The final styling is still being done with CSS.

<Image src={`/${image}`} alt={imageAlt} width={300} height={300} />

API Routes

api-route

How to Build a NextJS App

  • set up the core page
  • Layout (<main> tag)

_document.js vs. _app.js

You can use the _document to set lang, load fonts, load scripts before page interactivity, collect style sheets for CSS in JS solutions like Styled Components, among a few other things.

For example, applying a CSS class to the body of the page, as part of the render logic, is not possible in _app. It is only possible via side-effects.

And that's about it. Some often try to inject initial data, and other discouraged things in _document, which you're probably better off ignoring.

_app in the other hand, is more of a page initialiser, from the docs:

Next.js uses the App component to initialize pages. You can override it and control the page initialization. Which allows you to do amazing things like:

Persisting layout between page changes Keeping state when navigating pages Custom error handling using componentDidCatch Inject additional data into pages -Add global CSS All of that because _app actually does get executed at runtime! also you could use _document along with React Portals

Deploy NextJS Apps

deployment

  • deploy steps && considerations:

    deployment-1

  • .env.local always overrides the defaults set.

  • connect git with vercel

NextJS Auth

  • auth

  • auth-1

    • reason for using token instead of sessions:

      • pages are served directly and populated with logic without necessaryily hitting the server. NextJS could build pages which use get service-side props and therefore there will be a request handled by the server every time that page is being served. But you will also have many pages which are pre-generated. And once the user is on your website, many pages will not be fetched from the backend at all, but instead will be loaded and generated dynamically with front-end javascript because you still have a single-page application. You don't send a request for every page you visit because you're not using get server-side props on every page.

      • So to server, doesn't see every request which you send and therefore you load pages without the server being able to directly find out if you are authenticated or not. In addition, backend APIs which be used for a single-page applications are typically stateless. They don't care about the individual connected clients. They don't keep track of all the connected clients.Instead, the idea is that, that API can work pretty much on its own. And it just is able to hand out permissions to clients who authenticated so that they can then later request access to protect the resources.The API itself does not store any extra information about any connected clients. And since this is how we build single-page applications the server is not involved in every request and every action that's happening on our page because we handled that with front-end JavaScript and we have that stateless API connected to the SPA.

JWT:

jwt

  • You can unpack it and read the data inside of it without knowing that key. That key only proves that a given server created that token. But you can read the content of the token without knowing that key. The key of course will not be included in the token though.
  • And for example, if you wanna change our password we don't just send the old and the new password but we also include that token in the outgoing request. And that token then is validated by the server which basically checks... Okay. If I would use my signing key which only I know would I be able to generate this token? And if the answer is, yes. The server knows that it's valid. If the answer is no. It's invalid and access is denied.

Next Auth: