Eliminate Prop Drilling in Next.js with AsyncLocalStorage
This article explains how prop drilling in Next.js applications can be avoided by using Node.js's AsyncLocalStorage to store per‑request context such as requestId, tenantId, and userId, providing code examples, implementation steps, and practical usage tips for clean, maintainable backend logic.
Next.js is indeed an excellent framework, but it has some unfriendly aspects, especially prop drilling.
If you are tired of prop drilling or constantly passing the same data across multiple layers, you will love this tool.
🚧 Prop Drilling Problem
In typical Next.js applications, especially those using the App Router and server components, you often need to access shared context such as userId, tenantId, requestId, or feature flags.
When a requestId is generated in a server‑side function and you want to propagate it to all backend API calls, you must explicitly pass that field.
You end up passing the same value from one function to another through routers, services, loggers, and possibly all the way to database calls.
This is called Prop Drilling , which is especially annoying in backend logic where you don’t want business code cluttered with boilerplate context passing.
💡 Solution: AsyncLocalStorage
Node.js (since v13.10) provides a useful tool called AsyncLocalStorage. It allows you to store per‑request state that can be accessed anywhere in the request’s lifecycle without passing values.
It is somewhat like a per‑request globalThis, but it is scope‑isolated and safe for asynchronous calls.
Here is a simple example:
import { AsyncLocalStorage } from 'node:async_hooks';
const storage = new AsyncLocalStorage();
function logWithContext(message: string) {
const store = storage.getStore();
console.log(`[${store?.requestId ?? 'unknown'}] ${message}`);
}
storage.run({ requestId: 'abc123' }, () => {
logWithContext('User fetched data');
});🔥 Amazing! You no longer need to pass requestId —it is available exactly where you need it.
🧪 Real‑World Example: Next.js API Route Handler
Suppose you are building a Next.js app with API routes under app/api and you want each request to have a unique requestId that can be used in logs, database calls, etc.
Step 1: Set Up Storage
import { AsyncLocalStorage } from 'node:async_hooks';
type Context = {
requestId: string;
userId?: string;
};
export const asyncContext = new AsyncLocalStorage<Context>();Step 2: Wrap the Route Handler
import { asyncContext } from '@/lib/async-context';
import { v4 as uuid } from 'uuid';
import { logWithContext } from '@/lib/logger';
export async function GET() {
const requestId = uuid();
return asyncContext.run({ requestId }, async () => {
logWithContext('Handling request to /api/hello');
return new Response(JSON.stringify({ hello: 'world' }));
});
}Step 3: Use the Context Anywhere
import { asyncContext } from './async-context';
export function logWithContext(message: string) {
const store = asyncContext.getStore();
const tag = store?.requestId || 'unknown';
console.log(`[${tag}] ${message}`);
}No need to pass requestId around; just plug it in where needed.
🛠️ Using It in Practice
When guiding a junior developer, you can suggest using AsyncLocalStorage in the following scenarios:
Logging each request’s context (e.g., requestId)
Tagging tenant information in database queries ( tenantId)
Sharing user identifiers ( userId) across multiple asynchronous service layers
Accessing feature flags or locale in deeply nested logic
This keeps your logic clear and prevents code from becoming tangled—a subtle but powerful “advanced developer” technique.
⚠️ Caveats
Works only in the Node.js runtime; not applicable to Edge functions or middleware.ts.
Not suitable for long‑running background tasks; it only persists data within a request’s lifecycle.
May not persist across certain asynchronous boundaries (e.g., detached callbacks) unless handled carefully.
💬 Final Thoughts
Honestly, this pattern has saved me a lot of trouble. It isn’t as flashy as some cutting‑edge React tricks, but it solves a very practical problem in an elegant way—exactly the kind of solution you discover when working on production systems.
If you are building scalable Next.js and Node.js applications with complex API logic, add AsyncLocalStorage to your toolbox.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
