From jQuery to Next.js: Planning the Rewrite
jQuery has been a reliable tool for front-end development for years. But React and Next.js offer modern approaches to common problems — component reuse, declarative UI, server-side rendering — that can make a codebase easier to maintain and faster to deliver. Rewriting a jQuery site in Next.js is a significant effort, but the payoff comes in areas like build-time data fetching and smoother client-side transitions.
The fastest path is npx create-next-app, which scaffolds a complete project. To understand what's happening under the hood, it helps to build the same setup manually.
Start with npm init and accept the defaults — you'll replace them shortly. Then install React and Next.js:
npm install react react-dom next
Open package.json and swap the default scripts for these:
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
That gives you npm run dev for the development server, npm run build to create a production bundle, and npm run start to serve that build.
Where jQuery used an index.html file, Next.js uses a pages directory. Create it, add an index.jsx file inside, and write:
export default function Index() {
return <h1>Hello World</h1> ;
}
Run npm run start and visit localhost:3000 to see the h1 tag. The exported function's name is irrelevant, but avoid anonymous arrow functions — they break Next.js's fast refresh feature.
Styling Options in Next.js
With jQuery, you could link different stylesheets per page. Next.js supports that approach through next/head and a standard link tag, but there are more efficient ways built in.
Global Stylesheets
Create a custom App component in _app.js inside pages to apply CSS application-wide:
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
Then import any stylesheet at the top of that file. If you create a root-level styles folder with main.css, the import looks like this:
import "../styles/main.css"
Anything in that file applies to every page in the app.
CSS Modules
CSS modules generate unique class names from the classes you define, so the same class can appear multiple places without collisions. Alongside your page component, create an index.module.css file and import it:
import styles from "./index.module.css"
With a heading class defined in that file, you can apply it like this:
export default function Index() {
return <h1 className={styles.heading}>Hello World</h1> ;
}
Those styles apply only to that element.
Styled JSX
Styled JSX behaves like a scoped <style> tag. Add the jsx attribute and a template string:
<style jsx>{`
.heading {
font-weight: 700
`}</style>
Because it's runtime-evaluated, you can make values dynamic — for example, supplying font weight through component props:
<style jsx>{`
.heading{
font-weight: ${props.fontWeight}
`}</style>
The tradeoff: styled JSX adds runtime JavaScript to your bundle, about 12kb (3kb gzipped).
Handling Events Without jQuery
Where jQuery attaches global event handlers, React attaches them directly to elements using camelCase props. The familiar jQuery pattern for clicking a paragraph:
$( "p" ).click(function() {
console.log( "You clicked a paragraph!" );
});
Becomes this in React, where onclick becomes onClick:
export default function Index() {
function clickParagraph(){
console.log("You clicked a paragraph!");
}
return <p onClick={clickParagraph}>Hello World</p>;
}
jQuery makes it trivial to target all paragraphs at once. React requires per-element wiring, which is more verbose — but in a larger codebase it makes every interaction visible right where the element is defined, eliminating surprises from scripts that act on elements you've forgotten about.
Show and Hide vs. Conditional Rendering
jQuery's effects for toggling visibility — like the classic hide and show:
$( "p" ).hide();
— translate to React's conditional rendering. Combined with the event handler pattern above:
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>
)
}
Clicking changes show to false, and nothing renders. Expand that with a conditional operator to choose between two outputs:
show ? <p>Show this if show is true</p> : <p>Show this if show is false</p>
Fetching Data
jQuery's Ajax calls for external data have a React equivalent in the useEffect hook. This example fetches an exchange rate from a public API when the page mounts:
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 a function and a dependency array. The function performs async work using the fetch API, then sets state to update the page. The dependency array controls when the function runs — an empty array means it fires only on initial load.
Next.js also offers server-side fetching. getStaticProps runs at build time, which is a performance win since the data ships with the page rather than requiring a round trip. It only works in pages, not components:
export async function getStaticProps() {
return {
props: {},
}
}
Fetch whatever you need before the return, then pass data to the page through props.
Swap getStaticProps for getServerSideProps and the function runs on every request. This gives you access to Node.js APIs and lets you consolidate multiple data requests on the server, reducing what the client downloads.
A middle path is Incremental Static Regeneration. It builds pages statically like getStaticProps, but accepts a revalidate key — in seconds — that regenerates the page when requests come in, at most as often as you specify.
Rendering Lists Efficiently
jQuery's DOM manipulation has a sharp edge: looping over items and appending each one causes repeated DOM writes. React's virtual DOM computes diffs against the current state, so even items added in a loop reach the real DOM as one operation.
The JavaScript map function handles the iteration, converting each item to 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>
);
}
Each element produced by map requires a unique key prop. React uses it during diffing to tell elements apart, so uniqueness matters.
Promises Replace Deferreds
jQuery's deferred objects were designed to mirror native JavaScript promises, so the syntax translates almost directly. This is most visible in data fetching, where fetch returns a promise you can chain with .then:
fetch("example.com")
.then((response) => {
console.log(response)
})
.catch((error) => {
console.error(error)
})
That fetches example.com and logs the response, or logs the error if one occurs.
The newer async/await syntax is another option. Declare an async function:
async function myFunction(){
return
}
Inside it, prefix async calls with await:
async function myFunction(){
const data = await fetch("example.com")
return data
}
That promise resolves when the data arrives, so call it from within an async context. To catch errors properly, check the response status — if data.ok is false, throw an error — and wrap the await calls in a try/catch block instead of relying on .catch.
Where Next.js Goes Further
File-System Routing
Next.js's file-system routing resembles traditional multi-page sites, but adds dynamic routes. A blog with entries under /blog/* can use a file named [slug].jsx inside a blog folder; that component serves every URL under blog. The router tells you which path was requested:
const router = useRouter()
const { slug } = router.query
API Routes
API routes bring your backend into the same application. Create an api folder inside pages, and any file in it runs on the server rather than the client.
Each file exports a default function taking two parameters: the incoming request and the response object. A basic example:
export default function handler(request, response) {
response.status(200).json({ magazine: 'Smashing' })
}
What Doesn't Translate Directly
React ships without an official UI library analogous to jQuery UI. Both Reach UI and React Aria are popular community options focused heavily on accessibility, which broadens the audience your project can serve.
Animation is another gap. Conditional rendering can swap content in and out, but it can't fade elements out. React Transition Group fills that need by letting you define explicit enter and exit transitions.
Weighing the Migration Effort
Migrating a large codebase from jQuery to Next.js is a significant undertaking, but it is one that pays off in several important ways. The move gives you access to modern development concepts, including data fetching at build time, and it establishes a straightforward upgrade path for future releases of both React and Next.js. That means you are not just fixing today’s code structure; you are also preparing for the features that will come with tomorrow’s versions.
React’s component model is a real advantage for organizing code, which becomes increasingly important as a project scales. Along with that structure, React’s virtual DOM delivers a noticeable performance boost for many applications. Taken together, the architectural clarity and runtime gains make the migration effort worthwhile for most teams.
If you are planning this kind of transition, the following resources should help you along the way:
- “How To Migrate From jQuery To Next.js,” Facundo Giuliani
- “The What, When, Why And How Of Next.js’ New Middleware Feature,” Sam Poder
- “Localizing Your Next.js App,” Átila Fassina
- “How To Maintain A Large Next.js Application,” Nirmalya Ghosh




