In part 1 we covered some fundamentals of Next.js – rendering strategies along with the nuances of getStaticProps, getStaticPaths, getServerSideProps as well as data fetching.
Now we are going to talk about UI-related things, such as layouts, styles and fonts, serving statics, as well as typescript and environmental variables.
Built-in CSS support
Importing global styles
To add global styles, the corresponding table must be imported into the pages/_app.js
file (pay attention to the underscore):
// pages/_app.js import './style.css' // This export is mandatory by default export default function App({ Component, pageProps }) { return <Component {...pageProps} /> }
These styles will be applied to all pages and components in the application. Please note: to avoid conflicts, global styles can only be imported into pages/_app.js
.
When building the application, all styles are combined into one minified CSS file.
Import styles from the node_modules
directory
Styles can be imported from node_modules
, here’s an example of importing global styles:
// pages/_app.js import 'bootstrap/dist/css/bootstrap.min.css' export default function App({ Component, pageProps }) { return <Component {...pageProps} /> }
Example of importing styles for a third-party component:
// components/Dialog.js import { useState } from 'react' import { Dialog } from '@reach/dialog' import VisuallyHidden from '@reach/visually-hidden' import '@reach/dialog/styles.css' export function MyDialog(props) { const [show, setShow] = useState(false) const open = () => setShow(true) const close = () => setShow(false) return ( <div> <button onClick={open} className="btn-open">Expand</button> <Dialog> <button onClick={close} className="btn-close"> <VisuallyHidden>Collapse</VisuallyHidden> <span>X</span> </button> <p>Hello!</p> </Dialog> </div> ) }
Adding styles at the component level
Next.js supports CSS modules out of the box. CSS modules must be named as [name].module.css
. They create a local scope for the corresponding styles, which allows you to use the same class names without the risk of collisions. A CSS module is imported as an object (usually called styles
) whose keys are the names of the corresponding classes.
Example of using CSS modules:
/* components/Button/Button.module.css */ .danger { background-color: red; color: white; }
// components/Button/Button.js import styles from './Button.module.css' export const Button = () => ( <button className={styles.danger}> Remove </button> )
When built, CSS modules are concatenated and separated into separate minified CSS files, which allows you to load only the necessary styles.
SASS support
Next.js supports .scss
and .sass
files. SASS can also be used at the component level (.module.scss
and .module.sass
). To compile SASS to CSS you need to have SASS installed:
npm install sass
The behavior of the SASS compiler can be customized in the next.config.js
file, for example:
const path = require('path') module.exports = { sassOptions: { includePaths: [path.join(__dirname, 'styles')] } }
CSS-in-JS
You can use any CSS-in-JS solution in Next.js. The simplest example is to use inline styles:
export const Hi = ({ name }) => <p style={{ color: 'green' }}>Hello, {name}!</p>
Next.js also has styled-jsx support built-in:
export const Bye = ({ name }) => ( <div> <p>Bye, {name}. See you soon!</p> <style jsx>{` div { background-color: #3c3c3c; } p { color: #f0f0f0; } @media (max-width: 768px) { div { backround-color: #f0f0f0; } p { color: #3c3c3c; } } `}</style> <style global jsx>{` body { margin: 0; min-height: 100vh; display: grid; place-items: center; } `}</style> </div> )
Layouts
Developing a React application involves dividing the page into separate components. Many components are used across multiple pages. Let’s assume that each page uses a navigation bar and a footer:
// components/layout.js import Navbar from './navbar' import Footer from './footer' export default function Layout({ children }) { return ( <> <Navbar /> <main>{children}</main> <Footer /> </> ) }
Examples
Single Layout
If an application only uses one layout, we can create a custom app and wrap the application in a layout. Since the layout component will be reused when pages change, its state (for example, input values) will be preserved:
// pages/_app.js import Layout from '../components/layout' export default function App({ Component, pageProps }) { return ( <Layout> <Component {...pageProps} /> </Layout> ) }
Page-Level Layouts
The getLayout
property of a page allows you to return a component for the layout. This allows you to define layouts at the page level. The returned function allows you to construct nested layouts:
// pages/index.js import Layout from '../components/layout' import Nested from '../components/nested' export default function Page() { return { // ... } } Page.getLayout = (page) => ( <Layout> <Nested>{page}</Nested> </Layout> ) // pages/_app.js export default function App({ Component, pageProps }) { // use the layout defined ata page level, if exists const getLayout = Component.getLayout || ((page) => page) return getLayout(<Component {...pageProps} />) }
When switching pages, the state of each of them (input values, scroll position, etc.) will be saved.
Use it with TypeScript
When using TypeScript, a new type is first created for the page that includes getLayout
. You should then create a new type for AppProps
that overwrites the Component
property to allow the previously created type to be used:
// pages/index.tsx import type { ReactElement } from 'react' import Layout from '../components/layout' import Nested from '../components/nested' export default function Page() { return { // ... } } Page.getLayout = (page: ReactElement) => ( <Layout> <Nested>{page}</Nested> </Layout> ) // pages/_app.tsx import type { ReactElement, ReactNode } from 'react' import type { NextPage } from 'next' import type { AppProps } from 'next/app' type NextPageWithLayout = NextPage & { getLayout?: (page: ReactElement) => ReactNode } type AppPropsWithLayout = AppProps & { Component: NextPageWithLayout } export default function App({ Component, pageProps }: AppPropsWithLayout) { const getLayout = Component.getLayout ?? ((page) => page) return getLayout(<Component {...pageProps} />) }
Fetching data
The data in the layout can be retrieved on the client side using useEffect
or utilities like SWR
. Because the layout is not a page, it currently cannot use getStaticProps
or getServerSideProps
:
import useSWR from 'swr' import Navbar from './navbar' import Footer from './footer' export default function Layout({ children }) { const { data, error } = useSWR('/data', fetcher) if (error) return <div>Error</div> if (!data) return <div>Loading...</div> return ( <> <Navbar /> <main>{children}</main> <Footer /> </> ) }
Image component and image optimization
The Image
component, imported from next/image
, is an extension of the img
HTML tag designed for the modern web. It includes several built-in optimizations to achieve good Core Web Vitals
performance. These optimizations include the following:
- performance improvement
- ensuring visual stability
- speed up page loading
- ensuring flexibility (scalability) of images
Example of using a local image:
import Image from 'next/image' import imgSrc from '../public/some-image.png' export default function Home() { return ( <> <h1>Home page</h1> <Image src={imgSrc} alt="" role="presentation" /> </h1> ) }
Example of using a remote image (note that you need to set the image width and height):
import Image from 'next/image' export default function Home() { return ( <> <h1>Home page</h1> <Image src="https://blogs.perficient.com/some-image.png" alt="" role="presentation" width={500} height={500} /> </h1> ) }
Defining image dimensions
Image
expects to receive the width and height of the image:
- in the case of static import (local image), the width and height are calculated automatically
- width and height can be specified using appropriate props
- if the image dimensions are unknown, you can use the
layout
prop with thefill
value
There are 3 ways to solve the problem of unknown image sizes:
- Using
fill
layout mode: This mode allows you to control the dimensions of the image using the parent element. In this case, the dimensions of the parent element are determined using CSS, and the dimensions of the image are determined using theobject-fit
andobject-position
properties - image normalization: if the source of the images is under our control, we can add resizing to the image when it is returned in response to the request
- modification of API calls: the response to a request can include not only the image itself but also its dimensions
Rules for stylizing images:
- choose the right layout mode
- use
className
– it is set to the correspondingimg
element. Please note:style
prop is not passed - when using
layout="fill"
the parent element must haveposition: relative
- when using
layout="responsive"
the parent element must havedisplay: block
See below for more information about the Image component.
Font optimization
Next.js automatically embeds fonts in CSS at build time:
// before <link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet" /> // after <style data-href="https://fonts.googleapis.com/css2?family=Inter"> @font-face{font-family:'Inter';font-style:normal...} </style>
To add a font to the page, use the Head
component, imported from next/head
:
// pages/index.js import Head from 'next/head' export default function IndexPage() { return ( <div> <Head> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&display=optional" /> </Head> <p>Hello world!</p> </div> ) }
To add a font to the application, you need to create a custom document:
// pages/_document.js import Document, { Html, Head, Main, NextScript } from 'next/document' class MyDoc extends Document { render() { return ( <Html> <Head> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&display=optional" /> </Head> <body> <Main /> <NextScript /> </body> </Html> ) } }
Automatic font optimization can be disabled:
// next.config.js module.exports = { optimizeFonts: false }
For more information about the Head component, see below.
Script Component
The Script
component allows developers to prioritize the loading of third-party scripts, saving time and improving performance.
The script loading priority is determined using the strategy
prop, which takes one of the following values:
beforeInteractive
: This is for important scripts that need to be loaded and executed before the page becomes interactive. Such scripts include, for example, bot detection and permission requests. Such scripts are embedded in the initial HTML and run before the rest of the JSafterInteractive
: for scripts that can be loaded and executed after the page has become interactive. Such scripts include, for example, tag managers and analytics. Such scripts are executed on the client side and run after hydrationlazyOnload
: for scripts that can be loaded during idle periods. Such scripts include, for example, chat support and social network widgets
Note:
Script
supports built-in scripts withafterInteractive
andlazyOnload
strategies- inline scripts wrapped in
Script
must have anid
attribute to track and optimize them
Examples
Please note: the Script
component should not be placed inside a Head
component or a custom document.
Loading polyfills:
import Script from 'next/script' export default function Home() { return ( <> <Script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserverEntry%2CIntersectionObserver" strategy="beforeInteractive" /> </> ) }
Lazy load:
import Script from 'next/script' export default function Home() { return ( <> <Script src="https://connect.facebook.net/en_US/sdk.js" strategy="lazyOnload" /> </> ) }
Executing code after the page has fully loaded:
import { useState } from 'react' import Script from 'next/script' export default function Home() { const [stripe, setStripe] = useState(null) return ( <> <Script id="stripe-js" src="https://js.stripe.com/v3/" onLoad={() => { setStripe({ stripe: window.Stripe('pk_test_12345') }) }} /> </> ) }
Inline scripts:
import Script from 'next/script' <Script id="show-banner" strategy="lazyOnload"> {`document.getElementById('banner').classList.remove('hidden')`} </Script> // or <Script id="show-banner" dangerouslySetInnerHTML={{ __html: `document.getElementById('banner').classList.remove('hidden')` }} />
Passing attributes:
import Script from 'next/script' export default function Home() { return ( <> <Script src="https://www.google-analytics.com/analytics.js" id="analytics" nonce="XUENAJFW" data-test="analytics" /> </> ) }
Serving static files
Static resources should be placed in the public
directory, located in the root directory of the project. Files located in the public
directory are accessible via the base link /
:
import Image from 'next/image' export default function Avatar() { return <Image src="https://blogs.perficient.com/me.png" alt="me" width="64" height="64" > }
This directory is also great for storing files such as robots.txt
, favicon.png
, files necessary for Google site verification and other static files (including .html
).
Real-time update
Next.js supports real-time component updates while maintaining local state in most cases (this only applies to functional components and hooks). The state of the component is also preserved when errors (non-rendering related) occur.
To reload a component, just add // @refresh reset
anywhere.
TypeScript
Next.js supports TypeScript out of the box. There are special types GetStaticProps
, GetStaticPaths
and GetServerSideProps
for getStaticProps
, getStaticPaths
and getServerSideProps
:
import { GetStaticProps, GetStaticPaths, GetServerSideProps } from 'next' export const getStaticProps: GetStaticProps = async (context) => { // ... } export const getStaticPaths: GetStaticPaths = async () => { // ... } export const getServerSideProps: GetServerSideProps = async (context) => { // ... }
An example of using built-in types for the routing interface (API Routes):
import type { NextApiRequest, NextApiResponse } from 'next' export default (req: NextApiRequest, res: NextApiResponse) => { res.status(200).json({ message: 'Hello!' }) }
There’s nothing prevents us from typing the data contained in the response:
import type { NextApiRequest, NextApiResponse } from 'next' type Data = { name: string } export default (req: NextApiRequest, res: NextApiResponse<Data>) => { res.status(200).json({ message: 'Bye!' }) }
Next.js supports paths
and baseUrl
settings defined in tsconfig.json
.
Environment Variables
Next.js has built-in support for environment variables, which allows you to do the following:
use .env.local
to load variables
extrapolate variables to the browser using the NEXT_PUBLIC_
prefix
Assume we have .env.local
file, as below:
DB_HOST=localhost DB_USER=myuser DB_PASS=mypassword
This will automatically load process.env.DB_HOST
, process.env.DB_USER
and process.env.DB_PASS
into the Node.js runtime
// pages/index.js export async function getStaticProps() { const db = await myDB.connect({ host: process.env.DB_HOST, username: process.env.DB_USER, password: process.env.DB_PASS }) // ... }
Next.js allows you to use variables inside .env
files:
HOSTNAME=localhost PORT=8080 HOST=http://$HOSTNAME:$PORT
In order to pass an environment variable to the browser, you need to add the NEXT_PUBLIC_
prefix to it:
NEXT_PUBLIC_ANALYTICS_ID=value
// pages/index.js import setupAnalyticsService from '../lib/my-analytics-service' setupAnalyticsService(process.env.NEXT_PUBLIC_ANALYTICS_ID) function HomePage() { return <h1>Hello world!</h1> } export default HomePage
In addition to .env.local
, you can create .env
for both modes, .env.development
(for development mode), and .env.production
(for production mode) files. Please note: .env.local
always takes precedence over other files containing environment variables.
This concludes part 2. In part 3 we’ll talk about routing with Next.js – pages, API Routes, layouts, Middleware, and caching.