Some Thoughts on Request Memoization
With the advent of server-side components, we now have access to Request Memoization functionality. It allows us to cache requests within the same render pass. At the time of writing this article, this functionality was implemented in Next.js.
Imagine we have several components that require the same object for rendering. Forgetting about the existence of this functionality would require us to fetch this data at a higher level and pass it through props, context, or redux.
- Memoization only applies to the GET method in fetch requests.
- Memoization only applies to the React Component tree, which means:
- It applies to fetch requests in generateMetadata, generateStaticParams, Layouts, Pages, and other Server Components.
- It doesn't apply to fetch requests in Route Handlers as they are not a part of the React component tree.
- For cases where fetch is not suitable (e.g., some database clients, CMS clients, or GraphQL clients), you can use the React cache function to memoize functions. React extends fetch to automatically memoize fetch requests while rendering a React component tree.
Let's see how it works in practice.
Suppose I decided to sell some of my books. For this purpose, I want to develop a simple website consisting of 2 parts:
- Header with a title and a counter of selected books.
- List of books available for sale.
To display the number of items in the cart in the header and to mark which books from the list are already in the cart, we need to request the same object. For these purposes, I implemented a function:
export const getCart = async (info?: string) => { console.log("getCart", info) return fetch(`${process.env.API_URL}/cart`, { headers: { Cookie: getAllCookiesAsString(), }, }).then((res) => res.json() as Promise<Cart | null>) }
This function will be executed in the Header component:
import styles from "./styles.module.css" import { getCart } from "@/getCart" export const Header = async () => { const cart = await getCart("(Header)") return ( <header className={styles.Header}> <h1>Books</h1> <div>Cart: {cart?.books.length || 0}</div> </header> ) }
And in the BookList component:
import { getBooks } from "@/getBooks" import { getCart } from "@/getCart" import { UpdateCartButton } from "@/BookList/UpdateCartButton" export const BookList = async () => { const books = await getBooks() const cart = await getCart("(BookList)") return ( <ul> {books.map((book) => ( <li key={book.id}> {book.name} <UpdateCartButton cart={cart} bookId={book.id} /> </li> ))} </ul> ) }
Additionally, in the API, I added logs to ensure that this code is not executed after the results of execution have been cached.
export async function GET() { console.log("GET cart route") const { value: cartId } = cookies().get("cart") || {} const cart = await getCart(cartId) return Response.json(cart) }
As a result, getCart is called twice, while the GET method from the API is called only once.
The whole code can be viewed on github repository.