Next.js Overview
This is a general overview based on the course by Max Schwarzmüller on Udemy
What is Next.js?
Next.js is a React framework for production
Key features
Server-side (pre-) rendering of pages
- Prepares the content on the server
- Prebuilds content. No SEO issues, all content, metadata is there (unlike React)
File based routing
The structure of the files within the
pages
folder is the routingin the example above, routes are automatically created for
- / ⬅️ rendered by
index.js
- /contact ⬅️ direct child of
pages
- /posts ⬅️ rendered by
index.js
inside/posts
- /posts/[slug] ⬅️ filenames wrapped by square brackets route to any identifier
- / ⬅️ rendered by
Additionally, an api listens in
- /api/contact
Fullstack Capabilities
- Add backend (server side) code.
- Create APIs
- Can store data, get data, authenticate
- Can be added on top of a React app
How to start
run
npx create-next-app
or
yarn create next-app
Routes
- Pages created inside the
pages
folder will become routes. - Files thus inside can include normal React code.
- To create nested routes, create folders inside
pages
. - An
index.js
file in a nested folder will then be the route of the folder name, so/posts/index.js
will be a route on/posts/
. Collect-all
routes (with an indefinite amount of segments) are created with[...page].js
.
Dynamic routes
- To extract dynamic route data we can use the
useRouter
hook, so for the route/pages/[itemId]
:
import { useRouter } from 'next/router';
export default function DetailPage() {
const router = useRouter();
console.log(router.query.itemId) ⬅️ // this returns the value of the url, so if the url is /item2 that would be the console log
return <h1>Detail Page</h1>
}
useRouter
has most of the functions the react router has, such as
const router = useRouter();
router.push('/')
// or
router.replace('/') ⬅️ // which prevents going back to the previous page (useful for after submitting a form)
A dynamic path can be constructed to navigate programmatically like this:
function goToPage() {
router.push('/${props.pageId}')
}
Links
To navigate and link, import
import Link from 'next/link'
Link
expects an href
attribute and renders an anchor tag without sending a new request.
getStaticProps
The built-in function getStaticProps()
allows us to pre load data, so it runs the function before rendering the component, preloading the data inside of it.
All the code ran inside of it is server-side
and never exposed to the frontend, so it can access the file system, use credentials and run server tasks without compromising any information, since nothing ran in it would even be exposed in the frontend.
It is only executed during the building stage.
export async function getStaticProps() {
// fetch data from an API
// interact with the filesystem
// any server-based process!
}
It always returns an object, and passes it to the component with which it is interacting
export default function HomePage({ posts }) { // this props are received from getStaticProps
return (
<FeaturedPosts posts={posts} />
);
}
export function getStaticProps() {
const featuredPosts = getFeaturedPosts(); // gets my posts
return { // *always* returns an object
props: { // most of the times includes a 'props' object, that is returned to the above component.
posts: featuredPosts,
},
revalidate: 1800, // revalidate forces a new request after n seconds since the last one.
// Keep the number lower for data that needs to be more up-to-date, increase when extra requests aren't necessary.
};
}
getStaticServerProps
This function triggers a new regeneration for every new request.
Does not run on every build, but on every request.
export default function UserIdPage({ id }) {
return <h1>{id}</h1>;
}
export async function getServerSideProps(context) {
const { params } = context;
const userId = params.uid; ⬅️ // the file containing this functions is [uid].js
return {
props: {
id: `userid-${userId}`,
},
};
}
Besides params
, the context also receives a request
and a response
to interact with.
Note that req
and res
are not present in the context for getStaticProps
.
getStaticPaths
This function needs to be used if we are in a dynamic page and we are using getStaticProps
.
import path from 'path';
import fs from 'fs/promises'; ⬅️ // Possible because it's used inside getStaticProps!
export default function ProductDetailPage(props) { ⬅️ // Received from getStaticProps
const { loadedProduct } = props;
if (!loadedProduct) return <p>Loading...</p>;
return (
<>
<h1>{loadedProduct.title}</h1>
<p>{loadedProduct.description}</p>
</>
);
}
async function getData() {
const filePath = path.join(process.cwd(), 'data', 'dummy-backend.json');
const jsonData = await fs.readFile(filePath);
const data = JSON.parse(jsonData);
return data;
}
export async function getStaticProps(context) {
const { params } = context;
const productId = params.pid;
const data = await getData();
const product = data.products.find((prod) => prod.id === productId);
if (!product) {
return { notFound: true };
}
return {
props: {
loadedProduct: product,
},
};
}
export async function getStaticPaths() {
const data = await getData();
const ids = data.products.map((product) => product.id);
const pathsWithParams = ids.map((id) => ({ params: { pid: id } }));
return {
paths: pathsWithParams, ⬅️ // paths needs an array of all possible paths. In this case generated with the map method.
fallback: true,
};
}
getStaticPaths returns a list of dynamic paths for which the page should be regenerated on getStaticProps.
The paths
attribut getStaticPaths returns needs to contain an array as such
return {
paths: [
{
params: {
key: value ⬅️ // these are the values for the pages to be pre generated.
}
},
{
params: {
key: value ⬅️ // this typically happens with some constructor, like map().
}
}
]
}
getStaticPaths needs a fallback
attribute, which accepts true
, false
or "blocking"
fallback: false will render a 404 if the path is not found.
fallback: true will search for the missing page and eventually generate it on demand and afterwards cache it.
Blocking will block page render until data is fetched.
API Routes
Pages in the api
folder will be treated as api routes.
Api pages don't return jsx.
We can use credentials and secret data. Api routes only run in the server.
export default function handler(req, res) {
if (req.method === 'POST') {
// Only executes on post requests.
// allows to run server specific cope talking to the api.
// It's purely backend code
}
}
Connect to a database
Using MongoDb
as example, see the code below:
import { MongoClient } from 'mongodb';
export default async function handler(req, res) {
if (req.method === 'POST') {
const { email, name, message } = req.body;
const newMessage = {
email,
name,
message,
};
let client;
try {
client = await MongoClient.connect(process.env.DB_URI);
} catch (error) {
res.status(500).json({ message: 'Could not connect to database' });
return;
}
const db = client.db();
try {
const result = await db.collection('messages').insertOne(newMessage);
newMessage.id = result.insertedId;
} catch (error) {
client.close();
res.status(500).json({ message: 'Store message failed!' });
return;
}
client.close();
res
.status(201)
.json({ message: 'Message successfully sent!', feedback: newMessage });
}
}
Sending a request to the API
Conside the following code to send a post request to the previous api:
async function sendContactData(contactDetails) {
const response = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(contactDetails),
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (!response.ok) throw new Error(data.message || 'Something went wrong!');
}
async function sendMessageHandler(event) {
event.preventDefault();
try {
await sendContactData({
email: enteredEmail,
name: enteredName,
message: enteredMessage,
});
} catch (error) {
// Error handling
}
}
The Head element
The Head element renders in components (or in the app) and can wrap title, meta tags, etc.
The _document
file
The default _document.js
file lives in the pages
folder.
Its default content is:
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
It can be used to add additional properties, such as lang
, like this:
render() {
return (
<Html lang="en">
[...]