Next.js
Getting Started with Next.js
Introduction
Next.js is framework for building single page applications using React, Next.js allows us to use a combination of SSR and Prerendering to build our applications based on the data requirements
Notes from Next.js Documentation. This is slightly different to the documentation because I am using a Typescript setup
Setting Up
To set up a new Next.js application we will need install the relevant dependencies
mkdir web
cd web
yarn init -y
yarn add react react-dom next typescript @types/react @types/node
Then, add the following to your package.json
file:
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
Now, we can create the pages/index.tsx
with the following function component:
pages/index.tsx
import { NextPage } from "next"
const Index: NextPage = () => <h1>Hello world!</h1>
export default Index
And then run yarn dev
to start the Dev server. You should be able the index page on http://localhost:3000
Linking Pages
We can first create a new page about.tsx
in the pages
directory:
about.tsx
import { NextPage } from "next"
const About: NextPage = () => <h1>About</h1>
export default About
We can then create a link from our index
page to link to this using the next/link
component. Modify the index
page to do this like so:
index.tsx
import { NextPage } from "next"
import Link from "next/link"
const Index: NextPage = () => (
<>
<h1>Hello world!</h1>
<p>
<Link href="/about">
<a>About</a>
</Link>
</p>
</>
)
export default Index
The Link
is a wrapper component which only accepts an href
, any other attributes needed for our link need to be included in the a
component
Shared Components
Like you would expect from when we use React on its own we can create shared components. Components are created in the components
directory. We'll create a header and layout component and implement this on our pages
Header.tsx
import Link from "next/link"
import { NextPage } from "next"
const Header = () => (
<div>
<Link href="/">
<a>Home</a>
</Link>
<Link href="/about">
<a>About</a>
</Link>
</div>
)
export default Header
We can then create a Layout
component which works as a higher order component (HOC) to render the overall page layout given a Page Component:
Layout.tsx
import { NextPage } from "next"
import Header from "./Header"
const Layout = ({ children }) => (
<>
<Header></Header>
{children}
</>
)
export default Layout
export const withLayout = (Page: NextPage | NextPage<any>) => {
return () => (
<Layout>
<Page />
</Layout>
)
}
We can then implement the withLayout
function when exporting out relevant page components:
index.tsx
export default withLayout(Index)
about.tsx
export default withLayout(About)
Dynamic Pages
Query Params
We can create dynamic pages based on the data from the URL Query parameters a user has when visiting the page. We can update the index
page to have a list of posts that we will link to:
index.tsx
import { NextPage } from "next"
import Link from "next/link"
import { withLayout } from "../components/Layout"
const PostLink = ({ title }) => (
<li>
<Link href={`/post?title=${title}`}>
<a>{title}</a>
</Link>
</li>
)
const Index: NextPage = () => (
<>
<h1>Hello world!</h1>
<p>
<Link href="/about">
<a>About</a>
</Link>
</p>
<ol>
<PostLink title="First Post"></PostLink>
<PostLink title="Second Post"></PostLink>
<PostLink title="Third Post"></PostLink>
</ol>
</>
)
export default withLayout(Index)
We can then make use of the useRouter
hook to retrieve this from a post
page like so:
post.tsx
import { NextPage } from "next"
import { useRouter } from "next/router"
import { withLayout } from "../components/Layout"
const Post: NextPage = () => {
const router = useRouter()
return <h1>Post: {router.query.title}</h1>
}
export default withLayout(Post)
Route Params
Usually we may want to link to specific pages on our site and the above method of using query params for everything is not ideal, we can also set up dynamic urls such that a post's url will be something like /post/post_id
for example. Next.js allows us to build our routes using our folder structure as well as a special syntax for the filenames. For example a page like above may be in file like pages/post/[id].tsx
We can update our index
page to make use of this routing strategy by updating the PostLink
to use an id
:
index.tsx
import { NextPage } from "next"
import Link from "next/link"
import { withLayout } from "../components/Layout"
const PostLink = ({ id }) => (
<li>
<Link href="/post/[id]" as={`/post/${id}`}>
<a>{id}</a>
</Link>
</li>
)
const Index: NextPage = () => (
<>
<h1>Hello world!</h1>
<p>
<Link href="/about">
<a>About</a>
</Link>
</p>
<ol>
<PostLink id="First-Post"></PostLink>
<PostLink id="Second-Post"></PostLink>
<PostLink id="Third-Post"></PostLink>
</ol>
</>
)
export default withLayout(Index)
The href
is the link to the page as per our component setup, and the as
is the url to show in the browser
post/[id].tsx
import { NextPage } from "next"
import { useRouter } from "next/router"
import { withLayout } from "../../components/Layout"
const Post: NextPage = () => {
const router = useRouter()
return <h1>Post: {router.query.id}</h1>
}
export default withLayout(Post)
Note how this is almost exactly like in the previous case where we used the query parameter to populate our page but now we use the
id
Fetching Page Data
To fetch data we'll use isomorphic-unfetch
which works on both the browser and on the client side. We need to add this to our application:
yarn add isomorphic-unfetch
Wherever we need to use this we can simply import it with:
import fetch from "isomorphic-unfetch"
Next we'll create a basic page which displays the content from the tvmaze
api so that we have something to render. Update the Header
component to link to the tv
page that we will create after
Header.tsx
import Link from "next/link"
import { NextPage } from "next"
const Header = () => (
<div>
<Link href="/">
<a>Home</a>
</Link>
<Link href="/about">
<a>About</a>
</Link>
<Link href="/tv">
<a>TV</a>
</Link>
</div>
)
export default Header
We can then create a new pages/tv.tsx
file with the following component:
tv.tsx
import { NextPage } from "next"
import Layout, { withLayout } from "../components/Layout"
import fetch from "isomorphic-unfetch"
type ShowData = {
id: string
name: string
}
type TvResponse = {
show: ShowData
}[]
type TvData = {
data: ShowData[]
}
const Tv: NextPage<TvData> = props => {
return (
<Layout>
<h1>TV</h1>
<ul>
{props.data?.map(show => (
<li key={show.id}> {show.name} </li>
))}
</ul>
</Layout>
)
}
export default Tv
Note how in the above we make use of the
Layout
component as a normal component instead of the wrapper, this is because thegetInitialProps
function is only called directly from the page component and so this doesn't work if we use thewithLayout
function like we have above
Next, we will need to update our page's getServerSideProps
function which is an async
function that NextPage
s use to fetch data on the server side, this is called by the framework to get the properties for the page when rendering
tv.tsx
export const getServerSideProps: GetServerSideProps = async () => {
const res = await fetch("https://api.tvmaze.com/search/shows?q=batman")
const data = (await res.json()) as TvResponse
console.log(`Show data fetched. Count: ${data.length}`)
return {
props: {
data: data.map(el => el.show)
}
}
}
Styling
For styling components next.js
has a few different methods to style components, for the moment my preferred way of doing this is using CSS Modules. To create and use a module we need to do the following:
First create a Module for the component's CSS, this is just a normal CSS selector with the .module.css
extension. This essentially scopes the CSS
Header.module.css
.Header a {
color: red;
}
Next, in our tsx
we need to import the module like so:
Header.tsx
import styles from "./Header.module.css"
Thereafter we can make use of the different classes exposed in our module by assigning the className
attribute of a component to a a property of the imported style
const Header = () => <div className={styles.Header}>...</div>
If instead of locally scoped styles we would like to use global styles we need to create a pages/_app.tsx
file and import any stylesheets we need globally into that, the purpose for this all being in a single file is to avoid conflicting global styles
_app.tsx
import "../styles.css"
import { AppProps } from "next/app"
const MyApp = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />
}
export default MyApp
API Routes
API Routes are found in the pages/api
directory. These are simple functions which handle the relevant API Route
For a route without any parameter like the api/data
route we can have:
pages/api/data.tsx
import { NextApiRequest, NextApiResponse } from "next"
type Data = {
name: string
}
export default (req: NextApiRequest, res: NextApiResponse<Data>) => {
res.status(200).json({ name: "John Doe" })
}
Each API Endpoint has a request and response of NextApiRequest
and NextApiResponse
respectively
We can then consume this API on the client. To do this instead of using a simple fetch
we'll use SWR (Stale while Revalidate) which enables us to fetch data much more efficiently than if we were to make a normal HTTP request by using the cache to use temporary data
Install swr
with:
yarn add swr
Next, in our pages/index.tsx
we need to define a fetcher
function which will handle the fetching of data from the backend. This can be any asynchronous function which returns the data. We can also just pass in the fetch
function to make a normal get request and take the response directly
const fetcher = async (url: string) => {
const res = await fetch(url)
return (await res.json()) as { name: string }
}
This will then be used by the useSWR
hook in our component like so:
const { data, error } = useSWR("/api/data", fetcher)
let name = data?.name
if (!data) name = "Loading..."
if (error) name = "Failed to fetch data."
Putting this all together in our index
page we end up with the following
index.tsx
import { NextPage } from "next"
import Link from "next/link"
import { withLayout } from "../components/Layout"
import useSWR from "swr"
const fetcher = async (url: string) => {
const res = await fetch(url)
return (await res.json()) as { name: string }
}
const PostLink = ({ id }) => (
<li>
<Link href="/post/[id]" as={`/post/${id}`}>
<a>{id}</a>
</Link>
</li>
)
const Index: NextPage = () => {
const { data, error } = useSWR("/api/data", fetcher)
let name = data?.name
if (!data) name = "Loading..."
if (error) name = "Failed to fetch data."
return (
<>
<h1>Hello world!</h1>
<h2>{name}</h2>
<p>
<Link href="/about">
<a>About</a>
</Link>
</p>
<ol>
<PostLink id="First-Post"></PostLink>
<PostLink id="Second-Post"></PostLink>
<PostLink id="Third-Post"></PostLink>
</ol>
</>
)
}
export default withLayout(Index)
Run in Production
To run the application in production you simply need to build it with:
yarn build
And then start it with:
yarn start