Still Convinced That Multiple Fetch Requests Hit the Database Multiple Times? You’re Missing Out!
One of the common mistakes I've encountered while working with Next.js is the misconception that making separate data fetching requests in different components is inefficient or problematic. In this article, we'll explore this mistake, how caching works in Next.js, and best practices for using ORMs (Object-Relational Mappers) to manage data efficiently.
Understanding Data Fetching in Next.js
When building a Next.js application, it's common to have multiple components that need to display the same data. For instance, consider a homepage that contains two components: one for displaying a product's title and another for displaying its price. A common misconception is that making separate fetch requests in each component leads to unnecessary duplication of API calls. However, this isn't the case!
The Power of Caching
Next.js and React's caching mechanisms work together to optimize data fetching. When you make a fetch request in one component, React caches that data. If another component requests the same data later, React will serve it from the cache rather than making another network call. This behavior minimizes redundant API calls and improves performance.
Example Scenario: Fetching Product Data
Let’s consider an example where we fetch product data for our components.
// components/ProductTitle.js
export default async function ProductTitle() {
const res = await fetch('/api/product');
const product = await res.json();
return <h1>{product.title}</h1>;
}
// components/ProductPrice.js
export default async function ProductPrice() {
const res = await fetch('/api/product');
const product = await res.json();
return <p>Price: ${product.price}</p>;
}
In the above example, both ProductTitle
and ProductPrice
make separate fetch requests to the same API endpoint. However, thanks to React's caching mechanism, the second call to fetch the product data will not hit the server again; it will retrieve the data from cache.
Explanation: In this scenario, both components make independent fetch requests to the /api/product
endpoint. Since the data is cached after the first request, the second fetch in ProductPrice
retrieves the already cached data, avoiding an additional network call. This caching mechanism optimizes performance and resource usage.
When ORMs Come into Play
While fetch requests are well-handled by React’s caching, the same does not apply when using an ORM or ODM (Object-Document Mapper) to retrieve data. In these cases, you must implement caching manually to avoid multiple database queries.
Implementing Caching with ORMs
If you're using an ORM like Prisma, you can use React's caching utilities to ensure that repeated calls to the same function do not result in redundant database queries.
import { cache } from 'react';
const getProduct = async () => {
// Simulate an ORM query
return await prisma.product.findMany();
};
const cachedGetProduct = cache(getProduct);
// Using cachedGetProduct in your component
const ProductTitle = async () => {
const products = await cachedGetProduct();
return <h1>{products[0].title}</h1>;
};
const ProductPrice = async () => {
const products = await cachedGetProduct();
return <p>Price: ${products[0].price}</p>;
};
Explanation: In this example, we define a function getProduct
that fetches product data using Prisma. By wrapping this function with cache
, we ensure that if cachedGetProduct
is called multiple times within the same rendering cycle, it only executes the database query once. Subsequent calls will return the cached data.
Unstable Cache for Persistent Caching
In cases where you need the cache to persist even after the request goes from the server to the client, you can use the unstable cache feature provided by Next.js. This allows you to maintain the cached data across server-client boundaries.
import { unstable_cache } from 'next/cache';
const getProduct = async () => {
return await prisma.product.findMany();
};
const cachedGetProduct = unstable_cache(getProduct);
// Using cachedGetProduct in your component
const ProductTitle = async () => {
const products = await cachedGetProduct();
return <h1>{products[0].title}</h1>;
};
const ProductPrice = async () => {
const products = await cachedGetProduct();
return <p>Price: ${products[0].price}</p>;
};
Explanation: Here, we use unstable_cache
to wrap the getProduct
function. This feature allows the cache to persist even after the server response is sent to the client. As a result, both ProductTitle
and ProductPrice
can access the same cached data even if they are rendered in separate parts of the application.
Conclusion
In Next.js, it's essential to understand how data fetching and caching work to avoid the misconception that separate fetch requests in different components are problematic. By leveraging React's caching mechanisms effectively and using ORMs wisely, you can enhance the performance of your applications while maintaining clean and efficient code.
By following these best practices, you can ensure that your Next.js application remains optimized and responsive. Happy coding!