Code Review Best Practices: Improving Code Quality and Collaboration in Next.js π οΈ
Code reviews are a crucial part of any development process. They ensure that the code is not only functional but also clean, efficient, and maintainable. In the context of Next.js, where server-side rendering, API routes, and dynamic routing are common, code reviews can significantly improve the quality of the application. In this post, we'll dive into practical examples of how code reviews can improve Next.js applications using the App Router, focusing on real-world scenarios and corrections. π
1. Reviewing and Improving API Routes π
Original Code
Let's say you have a simple API route for fetching user data:
// app/api/users/[id]/route.js
import { getUserById } from '@/lib/db';
export async function GET(request, { params }) {
const user = await getUserById(params.id);
return new Response(JSON.stringify(user), { status: 200 });
}
Review Feedback
Error Handling: The API does not handle cases where the user is not found or when there is a database error.
Security: The response does not sanitize or validate the
id
parameter, which could lead to security vulnerabilities like SQL injection.
Improved Code
// app/api/users/[id]/route.js
import { getUserById } from '@/lib/db';
export async function GET(request, { params }) {
try {
const user = await getUserById(params.id);
if (!user) {
return new Response(JSON.stringify({ error: 'User not found' }), { status: 404 });
}
return new Response(JSON.stringify(user), { status: 200 });
} catch (error) {
return new Response(JSON.stringify({ error: 'Internal Server Error' }), { status: 500 });
}
}
Key Takeaways
Error handling is crucial in API routes to ensure that the application can gracefully handle unexpected situations.
Validation and sanitization of input parameters protect against security vulnerabilities.
2. Optimizing Server-Side Rendering (SSR) Logic π₯οΈ
Original Code
Here's an example of a server-side rendering (SSR) function in Next.js:
// app/page.js
import { getServerSideProps } from '@/lib/ssr';
export default async function HomePage() {
const data = await getServerSideProps();
return (
<div>
<h1>Home Page</h1>
<p>{data.message}</p>
</div>
);
}
Review Feedback
Data Fetching: The
getServerSideProps
function name is misleading, as it implies a Next.js-specific API rather than a custom function.Performance: The function could benefit from caching to reduce server load on repeated requests.
Improved Code
// app/page.js
import { fetchHomePageData } from '@/lib/ssr';
export default async function HomePage() {
const data = await fetchHomePageData();
return (
<div>
<h1>Home Page</h1>
<p>{data.message}</p>
</div>
);
}
// lib/ssr.js
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 60 }); // Cache for 1 minute
export async function fetchHomePageData() {
const cachedData = cache.get('homePageData');
if (cachedData) {
return cachedData;
}
const data = await fetch('https://api.example.com/home');
const jsonData = await data.json();
cache.set('homePageData', jsonData);
return jsonData;
}
Key Takeaways
Meaningful naming helps maintain clarity in your codebase.
Implementing caching can greatly improve performance, especially for pages with high traffic.
3. Enhancing Component Reusability with Props π¨
Original Code
A typical component might be hardcoded, making it less reusable:
// components/Button.js
export default function Button() {
return (
<button className="bg-blue-500 text-white px-4 py-2">
Click Me
</button>
);
}
Review Feedback
- Hardcoded Values: The button's text and styles are hardcoded, limiting its reusability across different parts of the application.
Improved Code
// components/Button.js
export default function Button({ text, color = 'blue', onClick }) {
return (
<button
onClick={onClick}
className={`bg-${color}-500 text-white px-4 py-2`}
>
{text}
</button>
);
}
// Usage example in app/page.js
import Button from '@/components/Button';
export default function HomePage() {
return (
<div>
<h1>Home Page</h1>
<Button text="Submit" color="green" onClick={() => alert('Submitted!')} />
<Button text="Cancel" color="red" onClick={() => alert('Cancelled!')} />
</div>
);
}
Key Takeaways
Props are essential for creating reusable components.
Avoid hardcoding values that may need to change depending on the context in which a component is used.
4. Refining State Management in Components π§
Original Code
Consider a component managing state for form input:
// components/Form.js
import { useState } from 'react';
export default function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = () => {
// Submit logic
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} />
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}
Review Feedback
- State Management: Separate state handling logic for each field increases code complexity and can lead to duplication.
Improved Code
// components/Form.js
import { useState } from 'react';
export default function Form() {
const [formData, setFormData] = useState({ name: '', email: '' });
const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prevData) => ({ ...prevData, [name]: value }));
};
const handleSubmit = () => {
// Submit logic
};
return (
<form onSubmit={handleSubmit}>
<input name="name" value={formData.name} onChange={handleChange} />
<input name="email" value={formData.email} onChange={handleChange} />
<button type="submit">Submit</button>
</form>
);
}
Key Takeaways
Consolidate state management to reduce duplication and simplify your code.
Use dynamic property names in your state updates to handle multiple inputs efficiently.
5. Improving Performance with Memoization β‘
Original Code
Sometimes, developers overlook performance optimizations in components:
// components/UserList.js
import { useState, useEffect } from 'react';
export default function UserList({ users }) {
const [filteredUsers, setFilteredUsers] = useState([]);
useEffect(() => {
const activeUsers = users.filter((user) => user.active);
setFilteredUsers(activeUsers);
}, [users]);
return (
<ul>
{filteredUsers.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Review Feedback
- Performance: The filtering operation runs on every render, even if the list of users hasnβt changed.
Improved Code
// components/UserList.js
import { useMemo } from 'react';
export default function UserList({ users }) {
const filteredUsers = useMemo(() => {
return users.filter((user) => user.active);
}, [users]);
return (
<ul>
{filteredUsers.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Key Takeaways
- Memoization with
useMemo
can prevent unnecessary re-calculations and improve performance, especially in components that perform expensive computations.
Code reviews in a Next.js application are not just about catching errors; theyβre about improving the overall quality and maintainability of the codebase. By applying these practical examples, you can ensure that your code is clean, efficient, and aligned with best practices. Remember to focus on key aspects such as error handling, performance optimization, reusability, and state management. Happy coding! π
Feel free to share your own code review tips and experiences in the comments below! π¬
Subscribe to my newsletter
Read articles from Vivek directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Vivek
Vivek
Curious Full Stack Developer wanting to try hands on β¨οΈ new technologies and frameworks. More leaning towards React these days - Next, Blitz, Remix π¨π»βπ»