createProxy
Build proxy-backed component families like Show.div or your own custom variants.
createProxy
createProxy is the function behind proxy-backed exports like Show.div, For.ul, or Mount.section. If you want to build that same shape for your own component family, this is the page to read.
The difficult part of createProxy is that it mixes runtime behavior and type-level behavior:
- at runtime, it is a
Proxythat resolves properties like.divor.Link - at type time, it uses
ProxyTypeandUtilinentRegisterso those properties can stay typed
The easiest way to understand it is to learn the runtime shape first and the type helpers second.
What createProxy does
The real signature is:
createProxy(base, renderForTag, category)It combines three things into one export:
- the base component itself
- intrinsic tag variants like
.div,.button,.section - plugin-backed variants resolved from
PluginManager
That is why a single export can behave like both a component and a property bag.
The three inputs
base
This is the default component you want people to use when they call the proxy directly.
For example, with Show, the base form is:
<Show when={user}>...</Show>renderForTag
This is the factory that answers the question: “If the user asks for .div or .button, how should that variant render?”
It usually returns a forwardRef wrapper that renders the requested tag with your custom behavior applied.
category
This is the plugin lookup bucket. It tells createProxy which PluginManager category to search first when someone accesses an unknown property.
Built-in categories in the package include show, for, repeat, mount, switch, and base, but the runtime also accepts custom string categories.
Resolution order
When you access a property like Clickable.div or Clickable.Link, the proxy resolves it in this order:
- existing property on the target
- plugin registered for the current
category - plugin registered for
base undefined
That first rule matters more than it looks.
- all built-in HTML tag variants are already present as existing properties
- once a plugin variant is resolved once, it gets cached onto the target and also becomes an existing property later
So the effective rule is: built-ins win first, then category plugins, then base plugins, and successful plugin lookups are cached.
Smallest working mental model
Before worrying about advanced types, this is the runtime shape you are building:
import { ComponentPropsWithRef, createElement, forwardRef } from 'react';
import { createProxy } from '@ilokesto/utilinent';
type ClickableProps = {
active?: boolean;
children?: React.ReactNode;
};
function BaseClickable({ children }: ClickableProps) {
return <>{children}</>;
}
const renderForTag =
(tag: any) =>
forwardRef(function Render(
{ active = false, children, ...props }: ClickableProps & ComponentPropsWithRef<any>,
ref: any,
) {
return createElement(tag, {
...props,
ref,
'data-active': active,
}, children);
});
const Clickable = createProxy(BaseClickable, renderForTag, 'clickable');At this point, the runtime idea is already in place:
Clickableis the base formClickable.divandClickable.buttonare generated from HTML tagsClickable.SomePlugincan be resolved later throughPluginManager
End-to-end example: building Clickable
If you want the shortest complete tutorial, this is the one to follow. The idea is to build a component family that:
- has one base form,
Clickable - has intrinsic variants like
Clickable.button - can later gain plugin-backed variants like
Clickable.Link
Step 1: define the props and base behavior
Start with the behavior you want every variant to share.
type ClickableProps = {
active?: boolean;
children?: React.ReactNode;
};
function BaseClickable({ children }: ClickableProps) {
return <>{children}</>;
}In this example, the base form does almost nothing by itself. That is normal. The interesting part is usually how each tag variant gets rendered.
Step 2: teach tag variants how to render
renderForTag is where family-wide behavior gets attached to each intrinsic element.
import { ComponentPropsWithRef, createElement, forwardRef } from 'react';
const renderForTag =
(tag: any) =>
forwardRef(function Render(
{ active = false, children, ...props }: ClickableProps & ComponentPropsWithRef<any>,
ref: any,
) {
return createElement(tag, {
...props,
ref,
'data-active': active,
}, children);
});This is the piece that makes Clickable.button and Clickable.a feel like one family instead of unrelated components.
Step 3: create the proxy
const Clickable = createProxy(BaseClickable, renderForTag, 'clickable');At runtime, that one line gives you:
<Clickable>Plain base form</Clickable>
<Clickable.button active>Save</Clickable.button>
<Clickable.a href="/docs">Read docs</Clickable.a>Step 4: add the type layer
If you stop at runtime behavior, the proxy works but TypeScript cannot describe the full surface well. This is where BaseTypeHelperFn and ProxyType come in.
import { BaseTypeHelperFn, ProxyType } from '@ilokesto/utilinent';
type BaseClickableType<X = object> = {
(props: X & ClickableProps): React.ReactNode;
};
interface BaseClickableTypeFn extends BaseTypeHelperFn {
type: BaseClickableType<this['props']>;
}
type ClickableType = ProxyType<BaseClickableTypeFn, 'clickable'>;
export const Clickable: ClickableType = createProxy(BaseClickable, renderForTag, 'clickable');Step 5: add plugin-backed variants later
Once the family exists, plugin registration becomes a separate concern.
import Link from 'next/link';
import { PluginManager } from '@ilokesto/utilinent';
PluginManager.register({
clickable: {
Link,
},
});Now Clickable.Link can be resolved by name.
That separation is worth remembering:
renderForTagdefines how intrinsic-tag variants behavePluginManagerdecides which extra named variants exist
Step 6: make plugin variants visible to TypeScript
declare module '@ilokesto/utilinent' {
interface UtilinentRegister {
clickable: {
Link: typeof Link;
};
}
}Without this step, runtime may work while TypeScript still complains.
Making it type-safe
The runtime proxy above works, but TypeScript will not know the shape you want unless you describe it.
This is what the package's own components do.
import { ComponentPropsWithRef, createElement, forwardRef } from 'react';
import { BaseTypeHelperFn, ProxyType, createProxy } from '@ilokesto/utilinent';
type ClickableProps = {
active?: boolean;
children?: React.ReactNode;
};
function BaseClickable({ children }: ClickableProps) {
return <>{children}</>;
}
const renderForTag =
(tag: any) =>
forwardRef(function Render(
{ active = false, children, ...props }: ClickableProps & ComponentPropsWithRef<any>,
ref: any,
) {
return createElement(tag, {
...props,
ref,
'data-active': active,
}, children);
});
type BaseClickableType<X = object> = {
(props: X & ClickableProps): React.ReactNode;
};
interface BaseClickableTypeFn extends BaseTypeHelperFn {
type: BaseClickableType<this['props']>;
}
type ClickableType = ProxyType<BaseClickableTypeFn, 'clickable'>;
export const Clickable: ClickableType = createProxy(BaseClickable, renderForTag, 'clickable');The important part is not memorizing every helper name. It is understanding what they mean:
BaseTypeHelperFnis the contract for “given some props, what callable component type should exist?”ProxyType<F, Category>says “take that callable type and add HTML tags plus registered plugin keys”- the category string must match the plugin bucket you want to read from later
Registering custom variants
Once your proxy exists, you can teach it new property names through PluginManager.
This is a different step from intrinsic tag generation. HTML tags are eagerly generated through renderForTag, but plugin-backed variants are resolved later by name from PluginManager.
import Link from 'next/link';
import { PluginManager } from '@ilokesto/utilinent';
PluginManager.register({
clickable: {
Link,
},
});Now the runtime can resolve a plugin-backed property lookup such as:
<Clickable.Link href="/login">
Login
</Clickable.Link>Whether family-specific props like active make sense on a plugin-backed variant depends on how your renderForTag logic and registered component interact. Do not assume every plugin-backed variant behaves exactly like an intrinsic tag variant unless you designed it that way.
Registering types for custom variants
Runtime registration alone is not enough if you want TypeScript to know that Clickable.Link exists.
For that you augment UtilinentRegister.
import Link from 'next/link';
declare module '@ilokesto/utilinent' {
interface UtilinentRegister {
clickable: {
Link: typeof Link;
};
}
}Now the ProxyType<..., 'clickable'> line can see the registered key and type it correctly.
When to use base
If you register under base, the plugin becomes available as a fallback for all proxy families that do not already resolve that property in their own category.
That is powerful, but it is also broad. Use base when the variant is truly cross-cutting.
Use a custom category when the variant belongs only to one proxy family.
Common mistakes
1. Starting from the types before the runtime idea
If ProxyType<this['props']>-style syntax feels opaque, stop there and go back to the runtime flow:
base -> renderForTag -> createProxy -> property access -> plugin lookup
That is the real model.
2. Forgetting that built-ins win first
If a property already exists on the target, plugin lookup will not override it.
3. Registering the runtime plugin but not the type
Without UtilinentRegister augmentation, your code may work at runtime but still feel broken in TypeScript.
4. Using base too broadly
base is convenient, but it affects every proxy family that falls back to it.
When createProxy is the right tool
Use it when:
- you want one base primitive plus many tag variants
- you want category-specific plugin extension
- you want your API to look like
Thing.divorThing.Link
Avoid it when a plain component is already enough. createProxy is for component families, not just for “one more wrapper.”
Related pages
- Extensibility for the broader extension story
- Composition for
SlotandSlottable - Conditional Rendering for real built-in examples of proxy-backed primitives