Context approach
Deliver public variables through React Context instead of a global. A server component reads process.env and passes the values to a client provider; client components read them with a hook.
Setup
// app/layout.tsx
import { PublicEnvProvider } from 'next-dynenv'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<PublicEnvProvider>{children}</PublicEnvProvider>
</body>
</html>
)
}PublicEnvProvider sends every NEXT_PUBLIC_* variable in process.env, and like PublicEnvScript it opts the routes under it into dynamic rendering.
Reading values
'use client'
import { useEnvContext } from 'next-dynenv'
export function ApiStatus() {
const { NEXT_PUBLIC_API_URL, NEXT_PUBLIC_APP_NAME = 'My App' } = useEnvContext()
return (
<p>
{NEXT_PUBLIC_APP_NAME} is talking to {NEXT_PUBLIC_API_URL}
</p>
)
}useEnvContext() returns { [key: string]: string | undefined } and throws if no provider sits above the component:
useEnvContext must be used within a EnvProvider or PublicEnvProviderServer components don't need the provider. They call env(), which reads process.env.
A typed hook
The context is an untyped dictionary. Wrap it once to get named, parsed fields:
// lib/use-app-env.ts
import { useEnvContext } from 'next-dynenv'
export function useAppEnv() {
const env = useEnvContext()
return {
apiUrl: env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8080',
appName: env.NEXT_PUBLIC_APP_NAME ?? 'My App',
debug: env.NEXT_PUBLIC_DEBUG === 'true',
features: (env.NEXT_PUBLIC_FEATURES ?? '').split(',').filter(Boolean),
}
}What context doesn't reach
The provider only feeds useEnvContext(). Keep these limits in mind:
env()andenvParsersdon't read context. In the browser they look atwindow.__ENV, which only the script approach creates. Without it they returnundefinedor your default. Parse context values yourself, as in the typed hook above.- Non-React code can't call hooks. API clients, SDK setup, and
instrumentation-client.tsneed the script approach. - Values still reach the HTML. The provider's props travel in the React Server Component payload embedded in the page, so context is not a way to hide anything. Only send public values.
Need both? Render PublicEnvScript in <head> and PublicEnvProvider around children. They read the same source.
Nesting providers
useEnvContext() returns the nearest provider's object. An inner provider replaces the outer one for its subtree; the two are not merged. To extend the public set for one section, pass a merged object to EnvProvider:
// app/admin/layout.tsx
import { connection } from 'next/server'
import { EnvProvider } from 'next-dynenv'
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
await connection()
const publicEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith('NEXT_PUBLIC_')))
return <EnvProvider env={{ ...publicEnv, NEXT_PUBLIC_ADMIN_MODE: 'true' }}>{children}</EnvProvider>
}Keys you add this way exist only in context. A server-side env('NEXT_PUBLIC_ADMIN_MODE') won't see them, so read them through the hook on the client side only.
When to pick it
Use the context approach when every consumer is a React client component and you like values flowing through the tree, for example in a design system or a provider-heavy app. Otherwise start with the script approach.
Related
- Custom variables: curated objects with
EnvProvider. - Components reference
- Context example