React framework for production
React apps:
- Not quite possible to build a full feature rich app ready to be deployed for prod
- React is a library for building user interfaces
- You have to make decisions on other features of the app like routing, styling, auth
Next js
- A package that uses react for building user interfaces
- Loaded with a lot more feautures that enable you to build full fledged prod ready app like routing, styling, auth
- react provides everything for you there's no need to install additional packages
- conventions need to be followed
Why?
- Simplifies the process of building a react app for prod
- File based routing
- Pre-rendenring
- API routes -> You can create APIs wwith NextJS
- Fullstack framework
- Support for CSS modules
- Authentication
- Dev and prod build system
Pre-requisites:
- HTML CSS and Javascript Fundamentals
- ES6 + Features
- React Fundamentals
Setting up environment:
- nodejs.org
- code.visualstudio.com
- To create a project wwe would need to execute:
npx create-next-app [project name]
- To start the project
npm run dev
Project structure:
- package.json : contains the dependencies and scripts for the project
- package-lock.json: auto-generated npm file
- next.cnfig.js : nextjs configuration file
- .eslint: to highlight errors and warns
- .next: generated by scripts
- node_modules: dependencies
- styles: contains the styles of the project
- public: holds the public resources for the app images, icons, etc
- pages: responsible for routing features of the app
- pages/index.js: gets served in the browser when we visit localhost in 3000
- pages/_app.js: we can define the layout for our app
- pages/api: to create apis
Flow control:
- When running npm run dev , the execution is tranfer to pages/_app.js which contains my app component
- It receives a Component and pageProps which are then retuned as part of the jsx
- The component will refers to the pages/index.js
Routing section:
-
Routing in a React app
- install a third party
- route.js file to configue the routes
- For each route , create a component file and configure the new route with a path
-
Routing next.js app
- File-system based routing mechanism
- when a file is added to the pages folder it automatically becomes available as a route
- mixing and matching file names with a nested folder structure, it is possible to pretty much define the most common routing patterns
-
Routing pages
-
pages folder is responsible for routing
-
scenario 1
- home page : localhost:3000
- create a file called index.js in pages
- create a functional component
-
scenario 2
- localhost:3000/about
- localhost:3000/profile
- create a file called about.js in pages with a functional component
- create a file called profile.js in pages with a functional component
-
scenario 3: Nested routes
- localhost:3000/blog
- localhost:3000/blog/first
- localhost:3000/blog/second
- create a folder called blog which will contain two files with their functional component. The blog/ will be pages/blog/index.js
-
scenario 4: Dynamic routes
- localhost:3000/product/ refer to a product list
- localhost:3000/product/:productId refers to different product
- create a file [productId].js in pages/product/
- recover the query param
import { useRouter } from "next/router"; const { query: { productId }} = useRouter();
- if the query param match with another component then the other component will be rendered
-
scenario 5: Nested dynamic routes
- localhost:3000/product/:productId/review/:reviewId
- create a folder called [productId]
- create an index.js for [productId] folder
- create a new folder for review
- create a new file called [reviewId].js inside the review folder
-
scenario 6 : catch all routes
- localhost:3000/docs/fature1/concept1/example1
- localhost:3000/docs/fature1/concept1/example2
- Sometimes we have a lot of params maybe infinite, so for that we would need to create multiple folders/files for each param
- We can create a file called [...params].js in order to get all the params
import { useRouter } from "next/router"; const { query: { params = [] }} = useRouter();
- Also we can add other brackets if we want to include the root of the folder as part of the [...params].js => [[...params]].js
-
-
Navigation
- we can navigate programmatically
- from home to blog
import React from "react"; import Link from "next/link"; const Home = () => { return ( <> <h1>Home page</h1> <Link href="/blog" replace> <a>Blog</a> </Link> </> ); }; export default Home;
-
Programmatically navigation
- If we have a ecommere or a submit button , we can navigate programmatically
import { useRouter } from "next/router"; const Home = () => { const router = useRouter(); const handleClick = () => { console.log("Placing your order"); router.push("/product"); //router.replace("/product/1"); }; }
-
Define a custom 404
- if you nevigate to a url that doesnt exist then nextjs will render a 404
- We can customize the 404 page
- create a 404.js
-
Pre - rendering
- What is pre rendering and why
- Types of pre-rendering
- static generation
- without data
- with data
- Incremental static generation
- dYNAMIC PARAMETERS WHEN FETCHING DATA
- Server side rendering
- data fetching
- Client-side data fetching
- COmbining pre rendering with client side data fetching
- static generation
-
Pre rendering
- comparing react app and next app
- react demo
- if we inspect the element we are going to see
- but when we check the view source code then the page loads we only see this tag
- the information that is sent from the server is empty and it doesnt contain any data
- next js demo
- we can see the same tags that are seen through inspect and view source code
Next js pre-renders every page in the app
- pre-render : generate html for each page in advance instead of having it all done by client side javascript
No pre-rendenring
- initial load app is not rendered --- js loads ---> Hydration: React components are intialized and app becomes interactive
Pre rendenring
-
pre rendered htmls is displayed --- js loads --> Hydration: React components are intialized and app becomes interactive
-
render HTML in advance with the necessary data for a page
Why pre-render?
-
improves performance
- in react app you need to wait for the javascript to be executed
- maybe fetch data form external api and then render UI
- there is a wait time for the user
- the html is already generated and loads faster
-
helps with SEO
- if you are building a ecommerce o blog it is very important to index the content
- with react app if the search enginer hits your page it only sees a div tag
- with a pre-render the content is present in the source code which will help index that page
-
How to pre-render? Next js supports two forms of prerendering
-
Static generation
-
A method of prerendering where HTML pages are generated at build time
-
The html wit all the data tat makes up the content of he we page are generated in advance whe you build your app
-
Recommended method to pre-render pages whenever possible
-
Can be built once , cached by CDN and served to the client almost instantly
-
ex: blog ecommerce, marketing pages and coumentation
-
How?
-
Next js by default prerender every page in our app
-
The HTML for every page will automatically be statically generated when build our app
-
Prod server - An optimized build is create once and you deploy that build. You dont make code changes on the go once it is deployed
- in nextjs a page will be pre-rendered once when we run the build command
-
Dev server - we should be able to make change in our code and we want that code to immediately reflect in the browser
- in next js the page is pre-rendered for every request you make
-
We dont have to be worried about static generation in dev mode
-
-
-
Static generation & data
-
Without data
- build the app and generate html content - no need to fetch external data
-
With data
-
Pages that can only be generated after fetching external data at build time
- build the app for production
- fetches external data
- The html can only be generated after fetching data
const Prerender = ({ users = [] }) => { return ( <> <h1>List of users</h1> {users.map((user) => ( <div key={user.id}> {user.name} - {user.email} </div> ))} </> ); }; export default Prerender; export async function getStaticProps() { const response = await fetch("https://jsonplaceholder.typicode.com/users"); const users = await response.json(); console.log({ data }); return { props: { users, }, }; }
-
getStaticProps
- pros
- we were able to fetch data and inject it to the page through props
- getStaticProps runs only on the server side
- The function will never run client side
- The code you write inside the getStaticProps wnt even be included in the JS bundle that is sent to the brwoser
- You can write server-side code directly in getStaticProps
- Accessing the file system usinf the fs module or querying a database cab be done inside getStaticProps
- You also dont have to be worry about including api keys
- cons
- it is allowed only in apage and cannot be run from a regular component file
- it is used only for prerendering and not client side data fetching
- should return an object and contain a props key
- get staticProps will run at build time
- pros
-
-
-
server side rendering
-
-
Pages vs components
- we would like to create components so we need to create a separated folder called components
-
Inspecting static generation build
- we should build our app for prod
- run : npm run build
- to serve we need to run: npm run start
- we should build our app for prod
-
Link pre fetching
- When a page with getStaticProps is pre-rendered at build time , in addition to the page HTML file Next js generates a JSON file holding the result of running getstatticprops
- The JSON file will be used in client-side routing through next/link or next/router
- CLient -side page transitions will not call getStaticProps as only the exported JSON is used
- Any component in the viewport will be prefetched by default (including the coressponding data) for pages using static generation this is why the load time is faster
- But if you navigate through url you wont have this ability
-
Summary Static generation
- is a method of prerendering where html pages are generated at build time
- with and without external data
- export getStaticProps function for external data
- HTML, Javascript and a JSON file are generated
- if you navifate directly yo the route, the html is served.
- If you navigate to the pae route from a different route, the page is created client side using the javascript and json prefetched from the server
-
Master detail pattern
- We have a master page and a details page that has the relevant details
- When click on articles then it will navigate to the article content
- we will have a dynamic parameter , so how we can mix static generation with a dynamic id
- dynamic parameters
export async function getStaticProps(context) { const { params } = context; const url = `https://jsonplaceholder.typicode.com/posts/${params.postId}`; const response = await fetch(url); const post = await response.json(); console.log({ post, params, url }); return { props: { post, }, }; } export async function getStaticPaths() { return { paths: [ { params: { postId: "1" } }, { params: { postId: "2" } }, { params: { postId: "3" } }, ], fallback: false, }; }
We would need to define every parameter in the getStaticPaths function
So with this the html will be loaded in advance at build time and when we navigate through the next/link or next/route it is going to be prefetched before going to the page
We can fetch the paths for getStatichPaths in order to avoid making individually
export async function getStaticPaths() { const url = `https://jsonplaceholder.typicode.com/posts`; const response = await fetch(url); const posts = await response.json(); const paths = posts.map((post) => ({ params: { postId: `${post.id}` } })); return { paths, fallback: false, }; }
fallback key is mandatory. It accepts three possible values false, true and 'blocking'
-
Fallback is false
- paths returned from getStaticPaths will be rendered to html at build time by getStaticProps
- if fallback set to false, any apths not retuned by getStaticPaths will result in 404 page
- when?
- it is suitable if you if you have an app with a small number of paths to pre-render
- when new pages are not added often
- blog site with a few articles is a good example for fallback set to false
-
Fallback is true
- paths returned from getStaticPaths will be rendered to html at build time by getStaticProps
- The paths that have not been generated at build time will not result in a 404 page, instead, next will serve a fallback version of the page on the first request to such a path.
- IN the back ground , next will statically generate the requested path HTML and JSON. thi includes running getStaticProps
- When that's done, the browser receives the json for the generated path. This will be used to automatically render the page with the required props. From the user's prespective, the page will be swapped from the fallback page to the full page.
- At the same time, next keeps track of the new list of pre-redenred pages. Subsequent requests to the same path will serve the generated page, just like other pages pre-redenred at build time
import React from "react"; import { useRouter } from "next/router"; const Post = ({ post }) => { const router = useRouter(); if (router.isFallback) { return <h1>Loading...</h1>; } return ( <div> <h1> {post.id} {post.title} </h1> <p>{post.body}</p> </div> ); }; export default Post; export async function getStaticProps(context) { const { params } = context; const url = `https://jsonplaceholder.typicode.com/posts/${params.postId}`; const response = await fetch(url); const post = await response.json(); if (!post.id) { return { notFound: true, }; } console.log("Generating page for postId: " + post.id); return { props: { post, }, }; } export async function getStaticPaths() { const url = `https://jsonplaceholder.typicode.com/posts`; const response = await fetch(url); const posts = await response.json(); // const paths = posts.map((post) => ({ params: { postId: `${post.id}` } })); const paths = [ { params: { postId: "1" } }, { params: { postId: "2" } }, { params: { postId: "3" } }, ]; return { paths, fallback: true, }; }
- When?
- If our app has a very large number of static pages (ecommerce)
- You want all the product pages to be pre-rendered but if you ave a few thousand products, builds can take a long time
- You may statically generate a samll subset of products that are ppular and use fallback for the rest
- When someone request a page that's not generated yet the user will see the page with a loading indicator
- shortly after, getStaticProps finishes and the page will be rendered with the requested data. From then onwards, everyone who requests the same page willl get statically pre-rendered page
-
Fallback blocking
- It is very similar to true
- The difference is that we wont see any content when the page is generated
- paths returned from getStaticPaths will be rendered to html at build time by getStaticProps
- The paths that have not been generated at build time will not result in a 404 page. Instead, on the first request, next js will render oon the server and return the generated HTML
- When it is dne the browser receives the HTML for the generated path. From the user perspective it will transition from 'the browser is requesting the page' to 'the full page is laoded'. There is no flash of loading/fallback state.
- Keeps track of the new list of pre-rendered
- When ?
- UX level preople prefer to be loaded without a loading indicator
- Some crawlers did not suppor javascript. The loading page would be rendered and then te full page would be laded which was causing problem
-
Summary static generation SSG
- Pros
- SSG is a methos of pre-renderin where the HTML pages are generated at build time
- The pre-rendered static pages can be pushed to a CDN, cached and served to clients acrosss the globe almost instantly
- SSG is fast and better for SEO as they are immediately indexed by search engines
- SSG with getStaticProps for data fetching and getStaticPaths for dynamic pages seems like a really good approach to a wide variety of applications in production
- Cons
- The build time is proportional to the number of pages in the app
- A page, once generated, can contain stale data till the time you rebuild the app
- e comerce product prices can vary everyday
- the entire app has to be re built and the page with updated data will be statically generated
- What about getStaticPaths?
- pre render only few pages at build time and rest of the pages can be prerendered on request
- can we not use that to render say 10000 most popular pages and rest of the 99000 pages can be prerendered on request
- it still does not fix the issue of stale data
- If you render 1000 pages at build time, and then the rest are generated based on incoming request, using fallback true or blocking, changes in data will not update the already pre-rendered pages
- So with an exmaple if we change information from the API, the static pages cannot take the new changes because there were generated either at build time or on request. So the next time we request the same page, we wont have the new value. For that reason there is a new concept called Incremental static regeneration.
- Pros
-
Incremental Static Generation (ISR)
- Allows you to update static pages after you have built your application
- You can statically generate indivicdual pages without needing to rebuild the entire site, effectively solving the issue of dealing with stale data
How?
- In the getStaticProps function, apart from the props key, we can specify a revalidate key
- The value for revalidate is the number of seconds after which a page re-generation can occur
What happen here?
-
If we define the number of second for revalidating for example 30 seconds, then: - The user make a request - In the background the server is waiting for 30 seconds - Within this time, we can modify information from the API - Also, within this time, you can make a lot of requests and you will see the same data and the same cached page - After 30 seconds, the server automatically invalid the current cached page and generate the new cached page. So, now the new cached page is available for the users return { - The user can request the same request and get a new cached page with new information
``` import React from "react"; const Product = ({ product }) => { return ( <div> <h1> {product.id} {product.title} </h1> <p>{product.price}</p> </div> ); }; export default Product; export async function getStaticProps(context) { const { params } = context; console.log({ params }); const url = `http://localhost:4000/products/${params.productId}`; const response = await fetch(url); const product = await response.json(); if (!product.id) { return { notFound: true, }; } console.log("Re - Generating page for productId: " + product.id); return { props: { product, }, revalidate: 20, }; } export async function getStaticPaths() { const paths = [{ params: { productId: "1" } }]; return { paths, fallback: "blocking", }; } ```
-
Sever side rendering (SSR)
-
SSG
- HTML is statically generated at build time. The build page is then cached and resued for each request
- For a dynamic page with getStaticPaths and fallback set to true the page is not generated at build time but is generated on initial request
- With incremental static regeneration, a page can be regenerated for a request after the revalidation time has elapsed
- For the most part, the pages are generated using getStaticProps when you build the project
- We have some problems
- We cannot fetch data at request time, we run into the problem of stale data
- With a very dynamic website
- getStaticProps will fetch news at build time which is not suitable at all
- getStaticPaths will help fetch the data on the initial request but it is ten cached for subsequent requests
- ISR can help but if revalidate is 1 seconde, we still might not always see the most up to date news when regeneration is happening at background
- fetch data on the clide side but not good for SEO
- With a very dynamic website
- We dont get access to the incoming requests
- We cannot fetch for specific users and get the userId for cookies
- We cannot fetch data at request time, we run into the problem of stale data
-
SSR
-
HTML is generated for every incoming request
-
It is required when you need to fetch data per request and also when you need to fetch personalized data keeping in min SEO
-
How?
-
We are building a website a list of articles
-
getServerSideProps
- It runs only in the server side
- the function will never run client -side
- The code you write inside getServerSideProps wont even be included in the js bundle that is sent to the browser
- You can write server-sde code (fs, querying database can be done)
- You wont hve to worry about including API Keys
- is allowed only in a page and cannot be run from a regular component file
- this is only for prerendering and not client side data fetching
- getServersideProps run at request time
import React from "react"; const NewsList = ({ articles }) => { return ( <div> <h1>List of new articles</h1> {articles.map((article) => ( <div key={article.id}> {article.title} | {article.category} </div> ))} </div> ); }; export default NewsList; export async function getServerSideProps() { const response = await fetch("http://localhost:4000/news"); const articles = await response.json(); return { props: { articles, }, }; }
-
-
-
Dynamic parameters
import React from "react"; const Category = ({ articles, category }) => { return ( <div> <h1>Article list for {category}</h1> {articles.map((article) => ( <div key={article.id}> {article.title} | {article.category} </div> ))} </div> ); }; export default Category; export async function getServerSideProps(context) { const { params: { category }, } = context; const response = await fetch( `http://localhost:4000/news?category=${category}` ); const articles = await response.json(); return { props: { articles, category, }, }; }
-
Context
export async function getServerSideProps(context) { const { params: { category }, req, res, query, } = context; console.log({ cookie: req.headers.cookie, query }); res.setHeader("Set-Cookie", ["name=Julian"]); const response = await fetch( `http://localhost:4000/news?category=${category}` ); const articles = await response.json(); return { props: { articles, category, }, }; }
-
-
Client side data fetching
-
You might not always need to pre render the data
-
Example: User dashboard page
-
No need to pre render data
-
components prerender an initial state and fetching with useEffect and useState we can create some calls asynchrnously
-
Example
import React from "react"; import { useState, useEffect } from "react"; const Dashboard = () => { const [isLoading, setisLoading] = useState(true); const [dashboardData, setDashboardData] = useState(null); useEffect(() => { const fetchData = async () => { const response = await fetch("http://localhost:4000/dashboard"); const data = await response.json(); setDashboardData(data); setisLoading(false); }; fetchData(); }, []); return isLoading ? ( <h2>Loading...</h2> ) : ( <div> <h1>Dashboard</h1> <h3>Posts - {dashboardData.posts}</h3> <h3>Likes - {dashboardData.likes}</h3> <h3>Followers - {dashboardData.followers}</h3> <h3>Following - {dashboardData.following}</h3> </div> ); }; export default Dashboard;
-
-
SWR vercel
- React hooks for data fetching
- It handles catching validation, etc
- swr.vercel.app
- npm i swr
- example:
import React from "react"; import useSWR from "swr"; const fetcher = async () => { const response = await fetch("http://localhost:4000/dashboard"); const data = await response.json(); return data; }; const DashboardSwr = () => { const { data, error } = useSWR("dashboard", fetcher); if (error) return "An error has occured"; if (!data) return "loading..."; return ( <div> <h1>Dashboard</h1> <h3>Posts - {data.posts}</h3> <h3>Likes - {data.likes}</h3> <h3>Followers - {data.followers}</h3> <h3>Following - {data.following}</h3> </div> ); }; export default DashboardSwr;
-
Pre - rendenring + client side data fetching
-
Event listing page
- A page that show a list of events happening around you
- SEO + request time request tie data fetching -> Server side rendering
- Client side data fetching for filtering events
- We can use shallow routing to set in the url params according to our filters
import React, { useState } from "react"; import { useRouter } from "next/router"; export async function getServerSideProps(context) { const { query } = context; const { category } = query; const queryString = category ? "category=sports" : ""; const response = await fetch(`http://localhost:4000/events?${queryString}`); const data = await response.json(); return { props: { eventList: data, }, }; } const Events = ({ eventList }) => { const fetchSports = async () => { const response = await fetch( "http://localhost:4000/events?category=sports" ); const data = await response.json(); setEvents(data); router.push("/events?category=sports", undefined, { shallow: true }); }; const [events, setEvents] = useState(eventList); const router = useRouter(); return ( <div> <h1>Events</h1> <button onClick={fetchSports}>Sports</button> {events.map((event) => ( <div key={event.id}> <h2> {event.id} {event.title} {event.date} | {event.category} </h2> <p>{event.description}</p> <hr /> </div> ))} </div> ); }; export default Events;
-
-
API Routes section intro
- API Routes feature and how to create a basic AI in next
- Handle GET and POST requests
- Dynamic API routes
- Handle DELETE requests
- Catch all APIs
- Next is a fullstack framework
- You can write the FE code in react and also write APIs that can be called by the front end code
- API outes allow you to create restful endpoints as part of your next js app folder structure
- Within the pages folder , you need to create a folder called
api
- Within that api folder you can define all the APIs for your app
- You can add business logic without needing to write any additional custome server code and without having to configure any api routes
- Next js ives you everything you need to write full stack react + node apps
-
First API services
- create a folder called api within the pages folder
- create an index file inside the api folder and it should export a function called handler with two parameters : request and response
export default function handler(req, res) { res.status(200).json({name: 'Home API route'}); }
- we can also create folders and files and they will be mapped as urls , similar to pages
- ./pages/api/dashboard/index.js (http://localhost:3000/api/dashboard)
export default function handler(req, res) { res.status(200).json({name: 'Dashboard API route'}); }
- The best thing is that the api folder will never be bundled in the frontend page so the browser cannot download the bundle
-
GET request
-
Create a folder called data in the root of the project
-
Create a file within the data folder
export const comments = [ { id: 1, text: `this is the first comment` }, { id: 2, text: `this is the second comment` }, { id: 3, text: `this is the third comment` }, ]
-
Create a new folder inside the api folder called comments
-
Create a new file called index inside the comments folder
import { comments } from "../../../data/comments"; export default function handler(req, res){ res.status(200).json(comments) }
-
Create a new folder inside the pages folder called comments
-
Create a new file called index inside the comments folder
import React, { useState } from "react"; const Comments = () => { const [comments, setComments] = useState([]); const fetchCommnents = async () => { const response = await fetch(`/api/comments`); const data = await response.json(); setComments(data); }; return ( <div> <button onClick={fetchCommnents}>Load comments</button> <hr /> {comments.map((comment) => ( <div key={comment.id}>{comment.text}</div> ))} </div> ); }; export default Comments;
-
-
POST request
-
Create a new button called submit in the frontend page
import React, { useState } from "react"; const Comments = () => { const [comments, setComments] = useState([]); const [comment, setComment] = useState(``); const fetchCommnents = async () => { const response = await fetch(`/api/comments`); const data = await response.json(); setComments(data); }; const submitComment = async () => { const response = await fetch(`/api/comments`, { method: "POST", body: JSON.stringify({ comment }), headers: { "Content-Type": "application/json", }, }); const data = await response.json(); console.log({ data }); }; return ( <div> <input type="text" value={comment} onChange={(e) => setComment(e.target.value)} /> <button onClick={submitComment}>Submit</button> <hr /> <button onClick={fetchCommnents}>Load comments</button> <hr /> {comments.map((c) => ( <div key={c.id}>{c.text}</div> ))} </div> ); }; export default Comments;
-
Add a new condition to handle the POST requests
import { comments } from "../../../data/comments"; export default function handler(req, res) { if (req.method === `GET`) res.status(200).json(comments); else if (req.method === "POST") { const comment = req.body.comment; const newComment = { id: Date.now(), text: comment, }; comments.push(newComment); res.status(201).json(newComment); } }
-
-
Dynamic API routes
-
For delete a comment you will know the comment id so in the url you should have something like /api/comments/:commentId
-
So we need to have a dynamic api routes
-
Similar to pages we can create dynamic routes by adding a new file with squeare brackets [commentId].js
import { comments } from "../../../data/comments"; export default function handler(req, res) { const { commentId } = req.query; const comment = comments.find( (comment) => comment.id === parseInt(commentId) ); res.status(200).json(comment); }
-
-
Delete request
-
Add new buttons to the front end to delete the comment
import React, { useState } from "react"; const Comments = () => { const [comments, setComments] = useState([]); const [comment, setComment] = useState(``); const fetchCommnents = async () => { const response = await fetch(`/api/comments`); const data = await response.json(); setComments(data); }; const submitComment = async () => { const response = await fetch(`/api/comments`, { method: "POST", body: JSON.stringify({ comment }), headers: { "Content-Type": "application/json", }, }); const data = await response.json(); console.log({ data }); }; const deleteComment = async (commentId) => { const response = await fetch(`/api/comments/${commentId}`, { method: "DELETE", }); const data = await response.json(); console.log({ data }); fetchCommnents(); }; return ( <div> <input type="text" value={comment} onChange={(e) => setComment(e.target.value)} /> <button onClick={submitComment}>Submit</button> <hr /> <button onClick={fetchCommnents}>Load comments</button> <hr /> {comments.map((c) => ( <div key={c.id}> {c.text} - <button onClick={() => deleteComment(c.id)}>Delete</button> </div> ))} </div> ); }; export default Comments;
-
Add a new logic in [commentId].js to handle delete method
import { comments } from "../../../data/comments"; export default function handler(req, res) { const { commentId } = req.query; const comment = comments.find( (comment) => comment.id === parseInt(commentId) ); if (req.method == "GET") { res.status(200).json(comment); } else if (req.method == "DELETE") { const index = comments.findIndex((c) => c.id === comment.id); comments.splice(index, 1); res.status(200).json(comment); } }
-
-
Catch all
- Sometimes the segments are optional we can get one segment or multiple segments
- for example: http://localhost:3000/api/one/two/three/four
- so for that we would need to create a new file called [...params].js
- in this file we could add
export default function handler(req, res) { const params = req.query.params; console.log({ params }); res.status(200).json(params); }
- We can get all the params through the variable req.query.params in this file
-
APIs and pre rendering
- We should not call our own api from pre rendering because it can cause unexpected behavior and the performance can be affected
-
Styling
-
Global styles
- by default next has a file called styles/globals.css
- global styles are applied to all components
- we can also add general css from external libraries in the _app.js
-
component level styling
-
css modules are added to implement specific modules
-
we can import css as a module and then we can add classes to the html elements
-
In the dom autogenerated class names are included
import React from "react"; import styles from "../styles/Home.module.css"; const contact = () => { return ( <> <h2>Contact</h2> <div className={styles.main}>Hello world</div> </> ); }; export default contact;
-
-
css in js solution with nextjs
- inline styles
- inline <h2 style={{color: 'orange'}}>
- styled-components
- npm i styled-components
- inline styles
-
-
APP LAYOUT
-
Layout with header and a footer
-
We can add Footer and Header compoennts in the folder components/
-
Then we can add them to the _app.js
import Footer from "../components/Footer"; import Header from "../components/Header"; import "../styles/globals.css"; import "../styles/layout.css"; function MyApp({ Component, pageProps }) { return ( <> <Header /> <Component {...pageProps} /> <Footer /> </> ); } export default MyApp;
- So in this case every page will have the same layout
-
What happen if a page has its own layout
-
We can define for each component a function called getLayout and create a new one
-
in about.js
import React from "react"; import Footer from "../components/Footer"; const About = () => { return <h2>About page</h2>; }; export default About; About.getLayout = function PageLayout(page) { return ( <> {page} <Footer /> </> ); };
-
in _app.js
import Footer from "../components/Footer"; import Header from "../components/Header"; import "../styles/globals.css"; import "../styles/layout.css"; function MyApp({ Component, pageProps }) { if (Component.getLayout) return Component.getLayout(<Component {...pageProps} />); return ( <> <Header /> <Component {...pageProps} /> <Footer /> </> ); } export default MyApp;
-
-
-
Head component
-
For SEO we can add a default header from next js
-
Inside the Head component we can add metas and title to the current page
import React from "react"; import Head from "next/head"; import Footer from "../components/Footer"; const About = () => { return ( <> <Head> <title>About</title> <meta name="description" content="Free tutorials" /> </Head> <h2>About page</h2> </> ); }; export default About;
-
-
Image component
-
next js offers an image component to deal with image challenges
-
we can have images inside the public folder
-
So in the first iteration we can add images with the native element
import React from "react"; const Cats = () => { return ( <div> {["1", "2", "3"].map((path) => ( <div key={path}> <img src={`/images/${path}.jpg`} alt="cat" width="280" height="420" /> </div> ))} </div> ); }; export default Cats;
-
However , we are addding all the weight of the image even if the size of the image is smaller
-
For that, we can add the Image compoennt from next js to optimize the load
import React from "react"; import Image from "next/image"; import img from "../public/1.jpg"; const Cats = () => { return ( <div> <Image src={img} placeholder="blur" alt="cat" width="280" height="420" /> {["1", "2", "3"].map((path) => ( <div key={path}> <Image src={`/${path}.jpg`} alt="cat" width="280" height="420" /> </div> ))} </div> ); }; export default Cats;
-
-
Absolute imports and module paths
-
nextjs makes very easy to add static
-
we can create a file called jsconfig.json and automatically it takes the new configuration
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/layout/*": ["components/layout/*"] } } }
-
-
Static html export
- there is a new command npm run export whichc exports all your pages to static HTML files that you can serve without the need of a node.js server
- cannot use ISR or SSR
- Client side data fetching for dynami content
- add in the scripts of the packege.json "export": "next build && next export"
-
Typescript support
-
create a file called tsconfig.json
-
run : npm run dev
-
We will have an error taht says that we were suposed to use typescript but we dont have the libraries, So we should copy and paste the libraries that we need for typescript
-
run : npm i -D typescript @types/react
-
If we run npm run dev again, then the tscondig will be updated
-
Also, a new file is created called next-env.d.ts which makes sure that the compiler will use typscript
-
We have an error because some modules cannot be found , this is because jsconfig.json is not in control. Instead of that the tsconfig.json is in control. SO for that we need to copy the modules we created in jsconfig to tsconfig
"compilerOptions": { "baseUrl": ".", "paths": { "@/layout/*": ["components/layout/*"] } ... }
-
Types:
- GetStaticProps, GetStaticPaths, GetServerSideProps from next
- NextApiRequest, NextApiResponse from next
-
-
Preview Mode
-
Help for CMS and helps users to create manage and modify content on a website
-
How preview mode can be used when you do hav a CMS
-
Pre rendering section we understood about static generation where the pages are prerendered at build time
-
However, it is not suitable when you are creating a draft in your cms and wan to preview draft changes immediately on your page
-
You want next js to by passs static generation for this scenario
-
There was a need to handle this scenario of preview of publish
-
Create in the pages/api folder a new file called preview.js
export default function handler(req, res) { res.setPreviewData({ user: "Julian Parra" }); // res.end("Preview mode enabled"); res.redirect(req.query.redirect); }
-
Create a page which will interact with preview view
-
In this case, create a file within /pages/prev/index.js
import React from "react"; const Prev = ({ data }) => { return <h1 className="content">Prev: {data}</h1>; }; export default Prev; export async function getStaticProps(context) { console.log(">>>>> Get static props <<<<", context.previewData); return { props: { data: context.preview ? "List of draft articles" : "List of published aticles", }, }; }
-
-
Each time we go to http://localhost:3000/api/preview?redirect=/prev in the browser, some cookies are set to indicate that preview mode is enabled
-
With a console log we can see that getStaticProps only once when executing:
- npm run build
- npm run start
-
However, if we enable preview view by going to http://localhost:3000/api/preview?redirect=/prev then the getStaticProps function execute each time the page is refereshed
-
To disable preview mode we can create a new api fucntion called pages/api/disable-preview.js
export default function handler(req, res) { res.clearPreviewData(); res.end("Preview mode disabled"); }
-
-
Redirect
-
This is useful when you are organizing and maybe you are going to maintainance you want to redirect to other page
-
in next.config.js
module.exports = { reactStrictMode: true, redirects: async () => { return [ { source: "/about", destination: "/", permanent: false, // false if this is temporary because of a maintanance, true it means that is permanent }, { source: "/old-blog/:id", // we can map parameters like :id for the destination as well destination: "/new-blog/:id", permanent: true, }, ]; }, };
-
-
Environment variables
- We require environment variables like dev, prod or qa
- We need to create a new file called .env.local
- To add variables we would need to add keys and values
DB_USER=Julian DB_PASSWORD=password
- We can get the info by calling process.env.DB_USER
- They are not exposed in the frontend by default
- but we can make it public by adding a prefix called NEXTPUBLIC*
-
Authentication
-
Intro
-
next-auth library
-
Authenticating with github
-
Handle signin sign out an securing the app
-
How to work with a database with mongodb
-
User :
- identitiy who user is
- access what permission he has
-
Autehtnication
- client side
- server side
- API routes
- Need to persist user data? if not we can use auth services like github, facebook, gmail.
-
Next Auth : Library that offers solutions for authentication
-
-
Setup
- Install the packages
- npm i next-auth
- Create a new folder in the pages/api called auth
- Inside the folder create a new file callesd [...nextauth].js
- We are going to sign in with GITHub, so before going to the code we ahve to create a new app in the github profile https://github.com/settings/developers
- Create a new app with http://localhost:3000 as Homepage URL and callback
- We have to copy the client Id and the secret Id
- Create a new file called .env.local
- Paste the clientId and the secret Id as GITHUB_ID and GITHUB_SECRET
- in the [...nextauth].js
import NextAuth from "next-auth"; import GitHub from "next-auth/providers/github"; export default NextAuth({ // add providers like github , gmail, twitter, facebook providers: [ GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET, }), ], });
- To sing in we have to go to the url http://localhost:3000/api/auth/signin
- It creates a new cookie called next-auth.session-token
- TO sing out we have to go to http://localhost:3000/api/auth/signout
- Install the packages
-
Sign in and sign out
- We want to navigate using sign in and sign out, in the navbar.js
import { signIn, signOut, useSession } from "next-auth/react"; function Navbar() { return ( <nav className="header"> <h1 className="logo"> <a href="#">NextAuth</a> </h1> <ul className={`main-nav loaded`}> <li> <Link href="/api/auth/signin"> <a onClick={(e) => { e.preventDefault(); signIn("github"); }} > Sign In </a> </Link> </li> <li> <Link href="/api/auth/signout"> <a onClick={(e) => { e.preventDefault(); signOut(); }} > Sign Out </a> </Link> </li> </ul> </nav> ) }
-
client side
-
we want to show the buttons according to the session state
-
before all, we have to wrap the application with the in the _apps.js file
-
We have to implement the useSession hook to get:
- data which contains an attribute called session
- status that is an wnum which cotnains three possible states: "loading" | "authenticated" | "unauthenticated"
-
To hide buttons accoding to the session in the navbar js we will have to add
import Link from "next/link"; import { signIn, signOut, useSession } from "next-auth/react"; function Navbar() { const { data: session, status } = useSession(); console.log({ session, status }); return ( <nav className="header"> <h1 className="logo"> <a href="#">NextAuth</a> </h1> <ul className={`main-nav ${status === "loading" ? "loading" : "loaded"}`}> {status === "unauthenticated" && ( <li> <Link href="/api/auth/signin"> <a onClick={(e) => { e.preventDefault(); signIn("github"); }} > Sign In </a> </Link> </li> )} {status === "authenticated" && ( <li> <Link href="/api/auth/signout"> <a onClick={(e) => { e.preventDefault(); signOut(); }} > Sign Out </a> </Link> </li> )} </ul> </nav> ); }
-
-
-
Securing pages
- only logged users can see profile page
- in the profile.js
import React, { useEffect, useState } from "react"; import { getSession, signIn } from "next-auth/react"; const Profile = () => { const [loading, setloading] = useState(true); useEffect(() => { const securePage = async () => { const session = await getSession(); console.log({ session }); if (!session) { signIn(); } else { setloading(false); } }; securePage(); }, []); if (loading) return <h3>Loading...</h3>; return <h1>Profile page</h1>; }; export default Profile;
-
Server side authentication
- to get session in the server side
import React from "react"; import { getSession } from "next-auth/react"; function Settings({ data }) { return ( <div> <h1>Settings page</h1> <h2>{data}</h2> </div> ); } export default Settings; export async function getServerSideProps(context) { const session = await getSession(context); return { props: { data: session ? "Yes session! :D" : "No session! :(", // we can send the session to the pageProps in the _app.js session, }, }; }
-
We could set the session in the session provider in _app.js
function MyApp({ Component, pageProps }) { if (Component.getLayout) return Component.getLayout(<Component {...pageProps} />); return ( <SessionProvider session={pageProps.session}> <Head> <title>Code julian</title> <meta name="description" content="Awesome tool" /> </Head> <Header /> <Navbar /> <Component {...pageProps} /> <Footer /> </SessionProvider> ); }
-
How to secure pages server side
- we can also evaluate from the server if the user can access to the current url if not we should redirect
import React from "react"; import { getSession } from "next-auth/react"; function Settings({ data }) { return ( <div> <h1>Settings page</h1> <h2>{data}</h2> </div> ); } export default Settings; export async function getServerSideProps(context) { const session = await getSession(context); if (session) return { props: { data: session ? "Yes session! :D" : "No session! :(", // we can send the session to the pageProps in the _app.js session, }, }; return { redirect: { destination: `/api/auth/signin?callbackUrl=http://localhost:3000/settings`, permanent: false, }, }; }
-
Securing API routes
import { getSession } from "next-auth/react"; const handler = async (req, res) => { const session = await getSession({ req }); if (!session) res.status(401).json({ error: "Unauthorized user" }); else res.status(200).json({ message: "Sucess", session }); }; export default handler;
-
Connecting to a database
-
Persist user data in the app
-
in mongodb atlas create a new account and a new cluster
-
run
- npm i mongodb @next-auth/mongodb-adapter
-
create env variables in .env.local
DB_USERNAME=xxx DB_PASSWORD=xxx DB_DATABASE=xxx DB_HOST=xxx DB_URL=mongodb+srv://$DB_USERNAME:$DB_PASSWORD@$DB_HOST/$DB_DATABASE?retryWrites=true&w=majority
-
In the pages/api/auth/[...nextauth].js
-