Migration From jQuery to Next.js: A Guide

Quick summary ↬
This guide will show you how to migrate your jQuery site to React with Next.js – which is a significant undertaking, especially for big code bases. However, this migration allows you to use newer concepts (such as data fetching at build time) to help with our code’s performance and maintainability.

jQuery has served developers well for many years. However, libraries (like React) and Frameworks (like Next.js) are now bringing us more modern features to help with our code’s performance and maintainability. This guide will show you how to rewrite your jQuery site using Next.js to take advantage of all these new features, such as client-side routing for smoother transitions and the ability to separate code into components to make it more reusable.

Getting started

The easiest way to get started with a Next.js is to run npx create-next-app. This will scaffold a project for you. However, to understand what this command does, we’ll create our application from scratch.

First, we’ll create our Next.js project using npm init. You can proceed with the default settings, as we will change them later. Then, we want to install React and Next.js using:

npm install react react-dom next

Next up, we can open the package.json file and replace the default scripts with:

"scripts": { "dev": "next", "build": "next build", "start": "next start"
}

This allows you to run npm run dev to start the development server; npm run build to build your application; and npm run start to start a server of that built application.

To add pages — like you would index.html with jQuery — create a directory named pages and create a file named index.jsx in it. Inside this file, place the following code:

export default function Index() { return <h1>Hello World</h1> ;
}

Now, by running npm run start and navigating to localhost:3000, you should see a h1 tag displayed. The name of this function isn’t important, so you can call it whatever you want. However, don’t use an anonymous arrow function, as this will prevent fast refresh from working.

More after jump! Continue reading below ↓

CSS

In jQuery, you can specify CSS by page, importing different stylesheets for different pages. This is also possible in Next.js using the next/head component and a link tag the same way as jQuery. Anyhow, there are more performance-friendly ways to to this in Next.js.

Global Stylesheet

The first way is with a global stylesheet. To do so, we need to create a custom App by making the file _app.js inside the pages directory. The starting point for this file is as follows:

function MyApp({ Component, pageProps }) { return <Component {...pageProps} />
} export default MyApp

At the top of this file, you can add an import statement and import any CSS file you want. For example, if you created a separate folder at the root level called styles and put main.css in it, then you would add:

import "../styles/main.css"

Now, whatever you put inside this file will be applied throughout your application.

CSS Modules

The next option is CSS modules — which allows you to specify CSS anywhere in your application. They will create unique class names from the classes you provide, so you can use a same class name in multiple places in your application’s code.

Expanding the initial hello world example, you could create a file index.module.css in the same directory and then write the import:

import styles from "./index.module.css"

Afterwards, if you were to define a heading class in the CSS file, you could do the following:

export default function Index() { return <h1 className={styles.heading}>Hello World</h1> ;
}

and those styles will be applied only to that element.

Styled JSX

The final built-in option is styled JSX. This is most similar to including a <style> tag at the top of your page to define some styles. Simply add jsx to the <style> tag, and use a template string inside, like this:

<style jsx>{` .heading { font-weight: 700 `}</style>

This option has the advantage of being changeable at runtime. For instance, if you wanted to supply the font weight in your component props, you could do:

<style jsx>{` .heading{ font-weight: ${props.fontWeight} `}</style>

The one disadvantage of this method is that it introduces additional runtime JavaScript into your application, increasing the size by 12kb (3kb gzipped).

Events

In jQuery, you might have events set up to respond to DOM elements. To give you an idea, you might want to execute code when a p tag is clicked and do so like this:

$( "p" ).click(function() { console.log( "You clicked a paragraph!" );
});

Instead, React uses event handlers — which you might have seen in HTML — like onclick. Note that React uses camelCase instead, and so onclick should be referenced as onClick. Therefore, rewriting this small example into React would look like this:

export default function Index() { function clickParagraph(){ console.log("You clicked a paragraph!"); } return <p onClick={clickParagraph}>Hello World</p>;
}

Each method comes with its advantages and disadvantages. In jQuery, it is easy to have something happen for all paragraphs, whereas in React, you have to specify per paragraph. However, for larger codebases, having to specify makes it easy to see what will happen with the interaction with any element, where you may have forgotten about the jQuery function.

Effects

Effects are used in jQuery to show and hide content. You might have something like this already:

$( "p" ).hide();

In React, this behavior is implemented using conditional rendering. You can see this by combining it with the replacement for events we just saw:

import {useState} from "react"
export default function Index() { const [show, setShow] = useState(true); function clickButton(){ setShow(false) } return ( <div> <h1>Hello world</h1> {show && <button onClick={clickButton}>Click me</button>} </div> )
}

When you click this button, it will change the value of show to false and so, the statement won’t render anything. This can be expanded with the conditional operator to show one thing or another, depending on the value like this:

show ? <p>Show this if show is true</p> : <p>Show this if show is false</p>

Data Fetching

In jQuery, Ajax is used for external data fetching without reloading. In React, this can be done by using the useEffect hook. For this example, we’ll fetch the exchange rate from a public API when the page loads:

import { useState, useEffect } from "react";
export default function Index() { const [er, setEr] = useState(true); useEffect(async () => { const result = await fetch("https://api.exchangerate.host/latest"); const exchangerate = await result.json(); setEr(exchangerate.rates["GBP"]); }, []); return ( <div> <h1>Hello world</h1> <p>Exchange rate: {er}</p> </div> );
}

useEffect takes in a function and a dependency array. The function does the data fetching, using async as the fetch API asynchronously. We can then set any state we want in there, and it will be updated on the page. The dependency array determines which value changes will run the function. In this case, it’s set to an empty array which means that the function will only run when the page first loads.

Beyond this, Next.js also provides options for fetching data on the server or at build time. For build time data fetching, the function getStaticProps can be used. This function provides an improvement in performance as the data can be provided with the page — rather than waiting on an external service. To use it, create this function in a page as it doesn’t work in components.

export async function getStaticProps() { return { props: {}, }
}

You can perform any data fetching you want before the return, and after that, pass the data through to the page under props — then, the data is provided to the page and can be accessed under the props.

By replacing the function name from getStaticProps to getServerSideProps, the function will be called on every request, giving you the flexibility to use Node.js functions if needed. It also allows you to make many data requests on the server and to process them to reduce the bandwidth used by the client.

You also have the option of a middle ground between the two called Incremental Static Regeneration. This option will generate a static page in the same way as getStaticProps, but it allows you to specify a revalidation period — which will regenerate the page when a request comes in at most as often as the period you specify. To do this, alongside props, you should also include a revalidate key with the time in seconds you want.

Objects into DOM elements

With jQuery, you have to be careful with which method you use for turning an object into DOM elements. The most common example of this is to create a list of items because, with jQuery, a loop over items would add each to the DOM one by one. With React, the virtual DOM is used to create diffs of the new state from the current one. This means that despite adding items in a loop, they are added to the real DOM as one operation.

This is done using the map function in JavaScript, where you can map each item to some JSX.

export default function Index() { const fruits = ["Apple", "Orange", "Pear"]; return ( <div> <h1>Hello world</h1> <ul> {fruits.map((fruit) => ( <li key={fruit}>{fruit}</li> ))} </ul> </div> );
}

Notice that the element inside the map needs a key prop. This is used in the diffing process discussed above, making it easy for React to distinguish between each element, so each of these should be unique.

Deffereds

The use of deferreds in jQuery can be replaced with the native JavaScript promise functionality. The syntax for deffereds was designed to mirror the functionality of promises, so the syntax should be familiar and not require too much alteration. One example of where deffereds might be used is in data fetching. If you do this with the fetch method in JavaScript, then you can add a .then to the end of the fetch as it returns a promise. This code will only run when the fetch is completed, and so the data (or an error) will be present. You can see this functionality in use here:

fetch("example.com")
.then((response) => { console.log(response)
})
.catch((error) => {
console.error(error)
})

This will fetch example.com and log the fetched response unless an error occurs — in this case it will be logged as an error.

In addition to this syntax, the newer async/await syntax can also be used. These require a function defined as async, in the same way as you might export a function. You can declare it like so:

async function myFunction(){ return
}

Inside this function, you can call further async functions by placing await in front of them, for example:

async function myFunction(){ const data = await fetch("example.com") return data
}

This code will return a promise that will resolve when the data is fetched, so it needs to be called inside an asynchronous function to await the result. However, in order to also catch errors, you will need to write a conditional to check the response status — if data.ok isn’t true, an error should be thrown. Then, you can wrap these away statements in a try catch block, rather than using .catch. You can read more about these methods in this article.

Improvements

Routing

Next.js uses file system routing, which is very similar to using different .html pages in a traditional website. However, this system also offers features beyond that, providing dynamic routes and allowing one page to be accessed under a range of urls.

For example, if you have a blog, you might keep all your files under /blog/*, creating a file [slug].jsx inside the blog folder — which will allow that content to be served for all pages under blog. Then, you can use the router in Next.js to find which route has been navigated to, like so:

const router = useRouter()
const { slug } = router.query

API routes

API routes allow you to also write your backend inside your Next.js application. To use these routes, create an api folder in your pages directory — now, any files created inside it will run on the server rather than the client, as with the rest of the pages.

To get started with these, you need to export a default function from the file, and this can take two parameters. The first will be the incoming request, and the second will let you create the response. A basic API route can be written like this:

export default function handler(request, response) { response.status(200).json({ magazine: 'Smashing' })
}

Limitations

jQuery UI

You may use jQuery UI in your application for user interface, but React doesn’t provide an official UI library like this. Nevertheless, a range of alternatives has been produced. Two of the most popular are Reach UI and React Aria. Both of these alternatives focus very strongly on Accessibility, ensuring that the project you create is usable by a bigger range of users.

Animation

While you can use conditional rendering instead of effects, this doesn’t provide all the same functionality, as you can’t do things such as fading content out. One library that helps to provide this functionality is React Transition Group — which allows you to define entering and exiting transitions.

Conclusion

Moving from jQuery to Next.js is a large undertaking, especially for big code bases. However, this migration allows you to use newer concepts (such as data fetching at build time) and sets you up to have simple migration paths to new versions of React and Next.js — along with the features they bring.

React can help you better organize your code (which is particularly important for large codebases) and brings a substantial performance improvement through the use of a virtual DOM. Overall, I believe that migrating from jQuery to Next.js is worth the effort, and I hope that if you decide to migrate, you enjoy all the features React and Next.js have to offer.

Further Reading on Smashing Magazine

Smashing Editorial
(nl)