Context Utility
Create contexts and scoped contexts with composable scopes for complex component libraries.
"use client";
Context Utility
The Context Utility provides a set of helpers to create both simple contexts and advanced “scoped” contexts. Scoped contexts let you have multiple, isolated instances of the same context on a page (e.g., nested menus, dialogs), and even compose scopes together for complex widgets.
Use createContext for a single global context. Use createContextScope when you need multiple instances (scopes) of a context or when you want to compose scopes across related components.
Why use it?
- Simple and typed Provider and useContext for plain contexts
- Scoped contexts to isolate multiple instances (e.g., nested collections, nested menus)
- Composable scopes for building higher-level components from multiple sub-contexts
- Strong TypeScript support with minimal boilerplate
API: createContext
Creates a standard (non-scoped) context with a typed provider and consumption hook.
const [Provider, useCtx] = createContext<ContextValue | null>(
'MyComponent', // root component name (used in error messages)
defaultContextValue? // optional default context value (makes context optional)
);Prop
Type
Returns
Prop
Type
Example: basic usage
import * as React from 'react';
import { createContext } from '@loke/ui/context';
type ThemeContextValue = { mode: 'light' | 'dark'; toggle: () => void };
const [ThemeProvider, useTheme] = createContext<ThemeContextValue>('Theme');
export function AppThemeProvider({ children }: { children: React.ReactNode }) {
const [mode, setMode] = React.useState<'light' | 'dark'>('light');
const toggle = React.useCallback(() => setMode(m => (m === 'light' ? 'dark' : 'light')), []);
return (
<ThemeProvider mode={mode} toggle={toggle}>
{children}
</ThemeProvider>
);
}
export function ThemedButton() {
const { mode, toggle } = useTheme('ThemedButton');
return (
<button onClick={toggle}>
Current: {mode}
</button>
);
}API: createContextScope
Creates a “scoped” context factory. Scopes allow you to have multiple independent instances of the same context in the tree and to compose multiple scopes together. This is especially useful for component libraries (menus, popovers, dialogs, collections) that may be nested or repeated.
const [createScopedContext, createScope] = createContextScope(
'MyScope', // scope name (used to namespace the scope)
deps? // optional: other scopes to compose with (advanced)
);Prop
Type
Returns
Prop
Type
Scoped Provider and Hook shape
- Scoped Provider props:
{ children: React.ReactNode; scope: Scope; ...contextFields } - Scoped hook signature:
useContext(consumerName: string, scope: Scope)
Example: building a scoped context
import * as React from 'react';
import { createContextScope, type Scope } from '@loke/ui/context';
type MenuContextValue = {
open: boolean;
setOpen: (v: boolean) => void;
};
const [createMenuContext, useMenuScope] = createContextScope('Menu');
// Build the actual context (scoped)
const [MenuProviderImpl, useMenuContext] =
createMenuContext<MenuContextValue>('Menu');
export function MenuProvider({
children,
scope,
}: { children: React.ReactNode; scope: Scope<MenuContextValue> }) {
const [open, setOpen] = React.useState(false);
// Merge parent scope(s) to provide correct scoping props
const scopeProps = useMenuScope()(scope);
return (
<MenuProviderImpl {...scopeProps} open={open} setOpen={setOpen}>
{children}
</MenuProviderImpl>
);
}
export function MenuTrigger({ scope }: { scope: Scope<MenuContextValue> }) {
const { open, setOpen } = useMenuContext('MenuTrigger', scope);
return (
<button type="button" onClick={() => setOpen(!open)}>
{open ? 'Close' : 'Open'} menu
</button>
);
}
export function MenuContent({
scope,
children,
}: { scope: Scope<MenuContextValue>; children: React.ReactNode }) {
const { open } = useMenuContext('MenuContent', scope);
return open ? <div role="menu">{children}</div> : null;
}In your app, create a scope instance and pass it through:
import * as React from 'react';
import { createContextScope } from '@loke/ui/context';
// We already created [createMenuContext, useMenuScope] above.
// Here we make a single scope instance for one menu.
const [/*ignore*/, useMenuScope] = [null, null] as unknown as ReturnType<typeof createContextScope>;
function Menu() {
const scope = useMenuScope(); // create a scope hook factory
const scopeProps = scope(); // a single scope instance for this menu
return (
<MenuProvider scope={scopeProps}>
<MenuTrigger scope={scopeProps} />
<MenuContent scope={scopeProps}>
{/* items */}
</MenuContent>
</MenuProvider>
);
}Example: composing multiple scopes
When building higher-level components, you may need to compose scopes from dependencies:
import { createContextScope } from '@loke/ui/context';
// Suppose Popover depends on Menu scope:
const [createMenuContext, useMenuScope] = createContextScope('Menu');
// Pass dependent scopes into createContextScope:
const [createPopoverContext, usePopoverScope] = createContextScope(
'Popover',
[useMenuScope] // compose with Menu’s scope
);
const [PopoverProvider, usePopoverContext] =
createPopoverContext<{ open: boolean; setOpen: (v: boolean) => void }>('Popover');
// In your PopoverProvider you can now do:
function PopoverProviderImpl({
children,
scope,
}: { children: React.ReactNode; scope: any }) {
const [open, setOpen] = React.useState(false);
const scopeProps = usePopoverScope()(scope); // composed scope across dependencies
return (
<PopoverProvider {...scopeProps} open={open} setOpen={setOpen}>
{children}
</PopoverProvider>
);
}Composing scopes is an advanced feature. Use it when a component implicitly depends on the scope of another component and should work seamlessly when nested or combined.
Patterns and tips
-
Optional vs. required context
- If you provide a defaultContext to createContext/createScopedContext, the hook will not throw outside of a Provider.
- If you omit it, useContext will throw with a helpful message unless wrapped by the appropriate Provider.
-
Memoization
- The Provider memoizes values by their prop values: pass stable references (e.g., via useMemo/useCallback) for best performance.
-
Scoping props
- For scoped contexts, always pass the correct scope to both Provider and consumers.
- Create a new scope instance when you need another isolated copy (e.g., multiple menus on a page).
-
Composition
- Use the createContextScopeDeps parameter to compose scopes when building higher-level components from lower-level building blocks.
Troubleshooting
-
“X must be used within Y”
- You are calling the hook outside of its Provider. Add the Provider or supply a defaultContext.
-
“Reading wrong instance”
- Ensure you pass the same scope object to both the scoped Provider and any consumer hooks in that subtree.
-
“Nested providers clash”
- If you need isolated state per instance, prefer scoped contexts; create a fresh scope instance per widget.
Types
- Scope: internal type used to thread scoped context instances
- CreateScope: a factory type used for composing scopes
import type { Scope, CreateScope } from '@loke/ui/context';This utility allows you to scale context usage from simple app‑wide state to advanced, nested component libraries with minimal boilerplate and strong typing.