A crash course of Next.js: UI-related and environmental (part 2) / Blogs / Perficient


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 the fill 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 the object-fit and object-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 corresponding img element. Please note: style prop is not passed
  • when using layout="fill" the parent element must have position: relative
  • when using layout="responsive" the parent element must have display: 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 JS
  • afterInteractive: 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 hydration
  • lazyOnload: 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 with afterInteractive and lazyOnload strategies
  • inline scripts wrapped in Script must have an id 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.





Source link

Social media & sharing icons powered by UltimatelySocial
error

Enjoy Our Website? Please share :) Thank you!