Introduction
Next.js is an open-source JavaScript framework created by Vercel to enhance React applications with features such as Server-Side Rendering and Static Site Generation.
Traditionally, React is used to create Single-Page Applications (SPAs) whose contents are rendered on the client side. Next.js extends this by allowing developers to create applications that can perform server-side actions, pre-fetch routes, and has support for TypeScript. On top of that - it doesn't require any extra configuration by default!
In this guide, we'll take a look at the pertinent features and workings of Next.js. To solidify the new knowledge, we'll build a full multi-page weather application that talks to external APIs and allows a user to record given states.
Link: The complete code for this application can be found on GitHub.
Installation and Setup
The easiest way to create a new Next.js application is by using the create-next-app
CLI tool. You can install it via npm
:
$ npm install create-next-app
Once installed, you can initialize a new Next.js application by calling the tool and supplying a name for your project:
$ npx create-next-app weather-app
Note: If you don't already have create-next-app
installed - npx
will prompt you to install it automatically.
Once the tool finished initializing a skeleton project, let's move to the directory and take a peek inside:
$ cd weather-app
$ ls
README.md node_modules/ package.json public/
next.config.js package-lock.json pages/ styles/
The standard package.json
, package-lock.json
and node_modules
are there, however, we've also got the /pages
, /public
and /styles
directories, as well as a next.config.js
file!
Let's take a look at what these are.
Features of Next.js
Next.js is ultimately an extension for React, and it does introduce a couple of new things that make React application development simpler and faster - starting with Next.js pages.
Pages
Next.js makes creating multi-page applications with React ridiculously easy with its default file-system-based router. You don't need to install any additional packages, such as react-router-dom
, or configure a router at all.
All Next.js projects include a default /pages
directory, which is the home of all of the React components you'll be using. For each component - a router will serve a page based on that component.
A React component is a page in Next's eyes, and is served automatically on the path corresponding to its filename.
For instance, suppose we create a component, contact.js
, within the /pages
directory:
const Contact = () => {
return (
<div>
Contact
</div>
)
}
export default Contact
The file-system-based router that Next.js employs will make this page accessible under the /contact
route! The only exception to this rule is the index.js
page, which isn't located under the /index
route, but rather, it's served at /
.
Additionally, you can nest routes with Next.js, so you can easily create a /weather/berlin
dynamically by creating a /weather
folder, and a dynamic [city].js
component to act as the page.
Note: For dynamic routes, you need to name the file in a set of square brackets. Without them, it's a static literal string, and it will be parsed as such. city.js
would resolve to the /weather/city
route, and wouldn't match for anything else. On the other hand [city.js]
would match for /weather/berlin
, /weather/london
, /weather/lagos
, etc.
The <Link> Component
The <Link>
component can be used to navigate between pages in your apps. Assuming our project page structure has several pages under the /pages
directory:
- pages
- index.js
- weather.js
- contact.js
The <Link>
component's href
attribute can be used to point to the relative path of each page, starting at the /pages
directory:
import Link from "next/link";
export default function Home() {
return (
<div>
<Link href="/">Home</Link>
<Link href="/weather">Weather</Link>
<Link href="/contact">Contact</Link>
</div>
)
}
Naturally, if you have a nested hierarchy of files, you can link to nested pages as well:
- pages
- weather
- [city].js
import Link from "next/link";
export default function Weather() {
return (
<div>
<Link href="/weather/berlin">Berlin</Link>
<Link href="/weather/london">London</Link>
<Link href="/weather/lagos">Lagos</Link>
</div>
)
}
The <Link>
component can also pre-fetch pages! Once a page has been loaded, and there are multiple links to other pages - if you know that a certain page is to be visited often, or would like to make sure that the page is loaded as soon as possible (without impacting the initial page), you can pre-fetch the page associated with a <Link>
to make the transition faster the smoother!
In fact, Next.js prefetches all pages by default. To that end, if you'd like to speed up a transition to a certain page, you set the
prefetch
attribute tofalse
for other pages.
For instance, it's conceivable that in a weather app, people are more likely to navigate to the /weather
route from the home page, rather than /about
. There's no need to pre-fetch the about.js
page/component, since you'd be burdening the server further for a page that isn't too likely to be clicked. On the other hand - weather.js
is most likely to be the next route people visit, so you can shave off some time in the transition by prefetching it:
import Link from "next/link";
export default function Home() {
return (
<div>
<Link prefetch=true href="/weather">Weather</Link>
<Link prefetch=false href="/about">About Us</Link>
</div>
)
}
Some other attributes include the scroll
attribute (which defaults to true
), which navigates the user to the top of the page when they reroute themselves with a <Link>
. This is a very sensible default, though you might want to turn it off for a more specific effect you'd like to achieve.
Another attribute worth noting is the replace
attribute, which defaults to false
. If set to true
, it'll replace the latest entry in the history stack, instead of pushing a new one, when you navigate to a new page/route with a <Link>
.
Pre-Rendering Pages
Speaking of prefetching and pre-rendering pages - this feature of Next.js is one of the more pertinent ones. Again, by default, Next.js will pre-fetch all of the pages that you link to, allowing for smooth, quick transitions between them.
For each page, you may choose between Server-Side Rendering or Static Generation and which technique is used depends on the functions you use to fetch data. You're not forced to adhere to one of these techniques for the entirety of the application!
Choosing between SSR and SG is a question of where you'd like to place the load, and both techniques prerender pages, just in a different way.
If you render your pages on the server-end, they'll be rendered on each request, using your server's resources, and sent to the end-user. If you statically generate a page, it's generated once and is reusable after build time.
Note: Generally speaking, you'll want to use Static Generation whenever there's no need to use Server-Side Rendering since the page can then be cached and reused, saving valuable computation. Whenever components on pages are frequent, Server-Side Rendering is required, and the page is rendered when requested with new data (some of which may depend on the request itself).
You may also decide to let some pages or elements on the page to be rendered via Client-Side Rendering which places the load on the end-user's machine, but you have no guarantees nor control over their resources, so typically, you want to avoid any intensive computation on their end.
What does pre-rendering entail in practice?
How does this impact the end-user, and how does it improve a plain React application? Pre-rendering allows a user to see the page before any of the JavaScript code is even loaded. It takes a really short while for the JavaScript to load - but these milliseconds inadvertently affect our perception. Even though the page is shown as it will be seen by the user once all components are loaded in - none of them work yet.
It is only once the page is shown that the components are being processed and loaded to become interactive components. This process is called hydration.
Without Next.js, the page would be empty while the JavaScript loads, and the components are being initialized.
Since pre-rendering is an integral part of Next.js, we'll take a look at some of the functions you can use to facilitate pre-rendering, both through SSR and SG.
Fetching Server-Side Data - getServerSideProps()
The getServerSideProps()
function is used to perform server-related operations such as fetching data from an external API. Again, you want to perform SSR whenever the data on the page changes rapidly, and it wouldn't make sense to cache it. For instance, an API may respond with updated stock prices, or the time on a clock every second, and on each request of the user - these should be up-to-date.
Here's an example that sends a request to a sample API and passes the data received as a prop to our page component:
const Weather = ({temperature}) => {
// display temperature
}
export default Weather
export async function getServerSideProps() {
const res = fetch('http://example.com/api')
...
const temperature = res.temperature
return {
props: {temperature},
}
}
The getServerSideProps()
receives a context
object, which contains server-related information such as incoming requests, server responses, queries. This is crucial, because the rendering itself may depend on the context
.
Static Generation Paths - getStaticPaths()
We use the getStaticPaths()
function to define the list of paths that should be statically generated for a dynamic route. Say we have a dynamic route pages/weather/[city].js
and we export a getStaticPaths()
function in this file like below:
export async function getStaticPaths() {
return {
paths: [{ params: { id: 'paris' } }, { params: { id: 'london' } }],
};
}
Next.js will automatically statically generate /weather/paris
and /weather/london
for us at build time.
Static Generation Props - getStaticProps()
The getStaticProps()
function is similar to getServerSideProps()
in the sense that we use it to load props on a pre-rendered page. In this case, however, the props are statically generated at build time and are reused for all requests later, instead of rendered at request time:
export async function getStaticProps() {
const res = await fetch('http://someapi/toget/cities')
...
const cities = await res.json()
return {
props: {
cities,
},
}
}
Note: getStaticPaths()
will not work with getServerSideProps()
- instead, use getStaticProps()
. It's best to utilize this function only when the data to be pre-rendered loads quickly or can be publicly cached.
<Head/> & SEO
Since Single-Page Applications are difficult to crawl by search engines, optimizing React applications for search engines may be difficult. While Next.js server-side rendering addresses this, the framework also includes a special <Head />
component that makes it simple to append elements to the head of your page.
As a result, updating your app pages' SEO settings like the title tag, meta-description, and any other element you'd include in a standard HTML <head>
tag is easier:
import Head from "next/head";
const Contact = () => {
return (
<div>
<Head>
<title>Contact</title>
<meta name="description" content="Welcome to our contact page!"/>
</Head>
</div>
);
};
export default Contact;
Creating API Routes with Next.js
Next.js also offers a feature to develop your own API right inside your project, and the process is similar to that of creating pages! To begin, you'll need to create a new api
subdirectory under /pages
(i.e. /pages/api
), and any file in this directory will be routed to /api/*
.
Say, we create a file
/pages/api/weather.js
. An endpoint is immediately accessible as/api/weather
.
To get these endpoints to work, you must export a default handler()
function (request handler) for each endpoint, which receives two parameters: req
(incoming request), and res
(server response).
To try this out, let's update our /pages/api/weather.js
example with the following content:
export default function handler(req, res) {
res.status(200)
res.json({
city: 'London',
temperature: '20',
description: 'sunny',
});
}
If we visit or send a request to /api/weather
, we should see the dummy weather information for London returned, as well as a 200
response code.
Static Assets
At some point, you'll probably want to load assets such as images, videos, fonts, and so on. All Next.js projects have a directory called /public
for this purpose.
Files in the
/public
directory can be accessed from anywhere in your application by preceding the source with the base URL (/
).
For example, if we have a file under /public/weather-icon.svg
, we can access it in any component with:
const WeatherIcon = () => {
return <img src="/weather-icon.svg" alt="Weather Icon"/>
}
export default WeatherIcon
Next.js Environment Variables
Environment variables are variables whose values are set outside of our application, and we mostly use them to keep sensitive data like API keys or server configurations to avoid pushing them to version control tools such as GitHub, GitLab, etc.
Next.js provides support for work with environment variables, through a .env.local
file. All the variables in this file are mapped to the process.env
.
If we have a .env.local
file with the following variables:
WEATHER_API_KEY=abcd123
CITY_API_KEY=123abc
We can now access them via process.env.WEATHER_API_KEY
and process.env.CITY_API_KEY
.
Also, environment variables aren't exposed in the browser by default and are only accessible in the Node.js environment (on the server side). We can however choose to expose them to the client-side by prefixing the preferred variable with NEXT_PUBLIC_
. For example, if we have a variable:
NEXT_PUBLIC_CITY_API_KEY=123abc
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
This variable can now be accessed anywhere in our application via process.env.NEXT_PUBLIC_CITY_API_KEY
.
Building a Weather App with Next.js
We've covered a lot of ground already! Solidifying new knowledge comes best through practice, so it's helpful to create an application to supplement the new theory.
We'll be building a weather application that detects the user's city and displays weather information based on that information. In addition, we'll be implementing a feature that allows users to save specific weather information at any moment and access it later.
The application will look something like this:
If you haven't already, create a new Next.js app with the command below:
$ npx create-next-app weather-app
And we can start our app with:
$ npm run dev
For simplicity's and brevity's sake, we'll be using Bootstrap to set up our application's interface instead of writing custom CSS. You can install Bootstrap using the command below:
$ npm install bootstrap
Once the installation is complete, let's open pages/_app.js
and include an entry for Bootstrap:
import "bootstrap/dist/css/bootstrap.css";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;
Note: The _app.js
file is the default App component that Next.js uses to initialize pages. It serves as the starting point for all of your page's components.
Now, we can make our app more visually appealing by changing the default font and adding a nice background color. Let's open styles/global.css
and make the following changes:
@import url('https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@100;200;300;400;500;800;900&display=swap');
body {
background: #4F32FF;
color: #fff;
font-family: 'Be Vietnam Pro', sans-serif;
}
That's more than enough to get going! Let's define the structure of our pages and placeholders for the data when it gets fetched via an API.
Page Markup
For our front-end, let's open up pages/index.js
and define the markup (structure) of our home page:
import Link from "next/link";
export default function Home() {
return (
<div>
<div
className="d-flex justify-content-center align-items-center"
style={{ minHeight: "100vh" }}
>
<div>
<div>
<h1 className="fw-bolder" style={{ fontSize: "60px" }}>
Null City.
</h1>
13 January, 2022
</div>
<div className="d-flex justify-content-between align-items-center mt-4">
<div className="pe-5">
<h2 className="d-inline">0</h2>
<sup>°C</sup>
<p className="text-info">Cloudy</p>
</div>
<div>
<img src="/1.png" alt="" width={100} draggable="false" />
</div>
</div>
<hr />
<div className="d-md-flex justify-content-between align-items-center mt-4">
<button className="btn btn-success border-0 save-btn px-4 py-3">
Timestamp
</button>
<Link href="/history">
<button className="btn btn-danger border-0 history-btn px-4 py-3 ms-auto">
My History
</button>
</Link>
</div>
</div>
</div>
</div>
);
}
Note: You'll need to download the weather icon from our GitHub, and include it in your project /public
folder.
And, at this point, if we preview our application in the browser, we should see the following output with dummy data:
Getting Weather Information
We will use a free weather API to get the user's current weather information, but because we want to display the weather information for the city the user is currently in, we'll need to use another API to get the user's current city and pass this parameter to the weather API in order to get the desired information.
The image below describes this process
To get the weather information, we will be making use of the OpenWeather API, and while they provide a free plan, you'll need to create an account in order to acquire an API key.
And to retrieve the user's city, we'll be making use of a free IP Geolocation API which does not require an API key to use.
Also, we want to make sure to display the weather information immediately after the page is loaded, so Next.js getServerSideProps()
does come in handy here!
Now, let's add the following exports to index.js
to perform all of the functions mentioned above:
export async function getServerSideProps() {
const ipRequest = await fetch(`http://ip-api.com/json/`);
const ipData = await ipRequest.json();
const city = ipData.regionName;
const api_key = 'YOUR_OPEN-WEATHER_API_KEY';
const url = `http://api.openweathermap.org/data/2.5/weather?q=${city},&appid=${api_key}&units=metric`;
const weatherRequest = await fetch(url);
const weatherInfo = await weatherRequest.json();
console.log(weatherInfo);
return { props: { weatherInfo, city } };
}
The code above performs two asynchronous operations:
- The first is to retrieve the user's city, which we store in a variable called
city
. - The second is to send a request to the weather API.
And finally, we've passed the result returned from the weather API, as well as the city as a prop to our index page.
Note: You'll need to replace YOUR_OPEN-WEATHER_API_KEY
with your own OpenWeather API key.
The required information is now stored as a prop for our index page in weatherInfo
and city
, and we can access them through:
...
export default function Home({ weatherInfo, city }) {
...
}
If you try logging weatherInfo
to the console, you'll notice that a lot of information is returned, including the user's coordinate and some other information that isn't required for our application. According to our application design, we will just need the following data:
- User's city
- Current temperature
- Weather description (e.g cloudy, light rain, snow, etc)
Finally, a weather icon based on the current temperature. The current temperature is returned at weatherInfo.main.temp
, and the weather description at weatherInfo.weather[0].description
.
So, let's go ahead and replace the dummy data in our markup with this information:
{/* ... */}
<div>
<h1 className="fw-bolder" style={{fontsize: "60px"}}>
{city}
</h1>
13 January, 2022
</div>
<div className="d-flex justify-content-between align-items-center mt-4">
<div className="pe-5">
<h2 className="d-inline">
{Math.round(weatherInfo.main.temp)}</h2>
<sup>°C</sup>
<p className="text-info text-capitalize">
{weatherInfo.weather[0].description}
</p>
</div>
<div><img src='/1.png' alt="" width={100} draggable="false" /></div>
</div>
{/* ... */}
We can also use the OpenWeather API to get a weather icon depending on the current temperature by simply passing the icon name as a parameter, and luckily this is also available at $weatherInfo.weather[0].icon
.
So, let's go ahead and replace the icon's <img>
tag with the code below:
{/* ... */}
<img
src={`http://openweathermap.org/img/wn/${weatherInfo.weather[0].icon}@2x.png`}
/>
{/* ... */}
And our application should be fully operational, displaying the current weather information based on the city we are currently in:
Saving Data Locally
Now, let's create a function that saves the current weather information, as well as the date and time it was stored in the browser's localStorage
. Each entry will be saved as an object with the following structure:
{
date: 'Current Date',
time: 'Current Time',
city: 'User\'s City',
temperature: 'User\'s city temperature',
description: 'Weather Description',
};
To do this, create a new function saveWeather()
(still inside our index.js
file) with the following code:
const saveWeather = () => {
const date = new Date();
let data = {
date: `${date.getDate()} ${date.getMonth() + 1} ${date.getFullYear()}`,
time: date.toLocaleTimeString(),
city: city,
temperature: weatherInfo.main.temp,
description: weatherInfo.weather[0].description,
};
let previousData = localStorage.getItem('weatherHistory');
previousData = JSON.parse(previousData);
if (previousData === null) {
previousData = [];
}
previousData.push(data);
localStorage.setItem('weatherHistory', JSON.stringify(previousData));
alert('Weather saved successfully');
};
The code above will parse any data previously stored in localStorage.weatherHistory
as JSON and depending on the type of data returned, we've pushed our new entry to an array, convert this array to string, and restore it in localStorage.weatherHistory
. We need to do this because localStorage
can only store strings and not any other data types.
If you'd like to read more about
localStorage
- read our Guide to Storing Data in the Browser with LocalStorage!
And, of course, we want to call this function when the user clicks the Timestamp button, so let's add an onClick
attribute to the button:
<button onClick={saveWeather}>Timestamp</button>
Weather History Page
Finally, we'll need to create a dedicated page to access all of the weather information that is saved in our browser's localStorage
.
Note: We won't be able to use Next.js data fetching functions because localStorage
or any other document object isn't available on the server, therefore we'll have to rely on client-side data fetching.
Create a new history.js
file under the pages
directory with the following content:
import { useState, useEffect } from "react";
const History = ({}) => {
const [weatherHistory, setweatherHistory] = useState([]);
useEffect(() => {
setweatherHistory(
localStorage.weatherHistory !== undefined
? JSON.parse(localStorage.weatherHistory)
: []
);
}, []);
return (
<div
className="d-flex justify-content-center align-items-center p-3"
style={{ minHeight: "100vh" }}
>
<div>
{" "}
<h2>My Weather History</h2>
<div className="mt-5">
{weatherHistory.length > 0 ? (
weatherHistory.map((weather, index) => {
return (
<div
key={index}
className="card mb-3"
style={{ width: "450px" }}
>
<div className="card-body text-dark">
<h5 className="card-title ">
{weather.city} - {weather.date}
</h5>
<small>{weather.time}</small>
<hr />
<p className="card-text">
<span className="font-weight-bold">Temperature: </span>
{weather.temperature}
<sup>°C</sup>
</p>
<p className="card-text">
<span className="font-weight-bold">Condition: </span>
{weather.description}
</p>
</div>
</div>
);
})
) : (
<p>Nothing to see here - yet</p>
)}
</div>
</div>
</div>
);
};
export default History;
The code above checks if localStorage.weatherHistory
exists, if it does - we parse the data and set it to a new variable weatherHistory
. If it doesn't, we've set this variable to an empty array instead.
In our markup, we check if there is at least one data entry in our weatherHistory
array, and using JavaScript's .map()
function, we iterate through all the items in weatherHistory
, displaying them in our webpage.
Let's go ahead and click the Timestamp button on the index page to record the current weather information, and when you return to the history page, you should see something like this:
Conclusion
Next.js is a JavaScript framework designed specifically to enhance and foster the development of performant React applications.
In this guide, we've gone through the pertinent features of the library - how pages are created and routed via Next.js' file-routing system, how the <Link>
component works, what prefetching and pre-rendering is and how to leverage it to enhance user experience, how API routes and request handlers can easily be created and how to work with environment variables.
To top it off - we built a weather application that communicates with external APIs to fetch data and display it to the end-user, allowing them to save any given timestamp to their local storage.
Again, the full source-code of the application is available on GitHub.