diff --git a/packages/react-native/README.md b/packages/react-native/README.md
index 3c09c67f..9b1cd585 100644
--- a/packages/react-native/README.md
+++ b/packages/react-native/README.md
@@ -195,6 +195,76 @@ Any prop value can be a dynamic expression resolved at render time:
See [@json-render/core](../core/README.md) for full expression syntax.
+### Computed functions
+
+Register named functions through `JSONUIProvider` or a component returned by `createRenderer` to resolve `$computed` expressions:
+
+```tsx
+ `${args.first} ${args.last}` }}
+>
+
+
+```
+
+### Custom directives
+
+Register custom directives through `JSONUIProvider` or a component returned by `createRenderer` to resolve user-defined `$`-prefixed values in component props:
+
+```tsx
+import { defineDirective, resolvePropValue } from "@json-render/core";
+import { z } from "zod";
+
+const math = defineDirective({
+ name: "$math",
+ schema: z.object({
+ $math: z.literal("multiply"),
+ a: z.unknown(),
+ b: z.unknown(),
+ }),
+ resolve(value, ctx) {
+ return Number(resolvePropValue(value.a, ctx)) *
+ Number(resolvePropValue(value.b, ctx));
+ },
+});
+
+const format = defineDirective({
+ name: "$format",
+ schema: z.object({
+ $format: z.literal("currency"),
+ value: z.unknown(),
+ currency: z.string(),
+ }),
+ resolve(value, ctx) {
+ const amount = Number(resolvePropValue(value.value, ctx));
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: value.currency,
+ }).format(amount);
+ },
+});
+
+
+
+;
+```
+
+The directives compose in props, so `$format` can format a `$math` result that reads its operands from state:
+
+```json
+{
+ "$format": "currency",
+ "value": {
+ "$math": "multiply",
+ "a": { "$state": "/price" },
+ "b": { "$state": "/qty" }
+ },
+ "currency": "USD"
+}
+```
+
+See the [directives documentation](https://json-render.dev/docs/directives) for more details.
+
## Tab Navigation Pattern
Combine `Pressable`, `setState`, visibility conditions, and dynamic props for functional tabs:
diff --git a/packages/react-native/src/renderer.tsx b/packages/react-native/src/renderer.tsx
index f9601a8b..3601918c 100644
--- a/packages/react-native/src/renderer.tsx
+++ b/packages/react-native/src/renderer.tsx
@@ -12,6 +12,9 @@ import type {
Catalog,
SchemaDefinition,
StateStore,
+ ComputedFunction,
+ DirectiveDefinition,
+ DirectiveRegistry,
} from "@json-render/core";
import {
resolveElementProps,
@@ -19,6 +22,7 @@ import {
resolveActionParam,
evaluateVisibility,
getByPath,
+ createDirectiveRegistry,
type PropResolutionContext,
type VisibilityContext as CoreVisibilityContext,
} from "@json-render/core";
@@ -40,6 +44,23 @@ import { ConfirmDialog } from "./contexts/actions";
import { standardComponents } from "./components/standard";
import { RepeatScopeProvider, useRepeatScope } from "./contexts/repeat-scope";
+const EMPTY_FUNCTIONS: Record = {};
+
+const FunctionsContext =
+ React.createContext>(EMPTY_FUNCTIONS);
+
+function useFunctions(): Record {
+ return React.useContext(FunctionsContext);
+}
+
+const DirectivesContext = React.createContext(
+ undefined,
+);
+
+function useDirectives(): DirectiveRegistry | undefined {
+ return React.useContext(DirectivesContext);
+}
+
/**
* Props passed to component renderers
*/
@@ -159,6 +180,8 @@ const ElementRenderer = React.memo(function ElementRenderer({
const { ctx } = useVisibility();
const { execute } = useActions();
const { getSnapshot } = useStateStore();
+ const functions = useFunctions();
+ const directives = useDirectives();
// Build context with repeat scope (used for both visibility and props)
const fullCtx: PropResolutionContext = useMemo(
@@ -169,9 +192,11 @@ const ElementRenderer = React.memo(function ElementRenderer({
repeatItem: repeatScope.item,
repeatIndex: repeatScope.index,
repeatBasePath: repeatScope.basePath,
+ functions,
+ directives,
}
- : ctx,
- [ctx, repeatScope],
+ : { ...ctx, functions, directives },
+ [ctx, repeatScope, functions, directives],
);
// Evaluate visibility (now supports $item/$index inside repeat scopes)
@@ -439,6 +464,10 @@ export interface JSONUIProviderProps {
string,
(value: unknown, args?: Record) => boolean
>;
+ /** Named functions for `$computed` expressions in props */
+ functions?: Record;
+ /** Custom directives for user-defined `$`-prefixed dynamic values */
+ directives?: DirectiveDefinition[];
/** Callback when state changes (uncontrolled mode) */
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
children: ReactNode;
@@ -454,9 +483,16 @@ export function JSONUIProvider({
handlers,
navigate,
validationFunctions,
+ functions,
+ directives,
onStateChange,
children,
}: JSONUIProviderProps) {
+ const directiveRegistry = useMemo(
+ () => (directives ? createDirectiveRegistry(directives) : undefined),
+ [directives],
+ );
+
return (
-
- {children}
-
-
+
+
+
+ {children}
+
+
+
+
@@ -663,6 +703,10 @@ export interface CreateRendererProps {
onAction?: (actionName: string, params?: Record) => void;
/** Callback when state changes (uncontrolled mode) */
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
+ /** Named functions for `$computed` expressions in props */
+ functions?: Record;
+ /** Custom directives for user-defined `$`-prefixed dynamic values */
+ directives?: DirectiveDefinition[];
/** Whether the spec is currently loading/streaming */
loading?: boolean;
/** Fallback component for unknown types */
@@ -716,9 +760,16 @@ export function createRenderer<
state,
onAction,
onStateChange,
+ functions,
+ directives,
loading,
fallback,
}: CreateRendererProps) {
+ const directiveRegistry = useMemo(
+ () => (directives ? createDirectiveRegistry(directives) : undefined),
+ [directives],
+ );
+
// Wrap onAction with a Proxy so any action name routes to the callback
const actionHandlers = onAction
? new Proxy(
@@ -744,15 +795,19 @@ export function createRenderer<
>
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/skills/react-native/SKILL.md b/skills/react-native/SKILL.md
index f43134b8..b31ad49d 100644
--- a/skills/react-native/SKILL.md
+++ b/skills/react-native/SKILL.md
@@ -131,6 +131,20 @@ Any prop value can be a data-driven expression resolved at render time:
Components do not use a `statePath` prop for two-way binding. Use `{ "$bindState": "/path" }` on the natural value prop instead.
+### Computed functions
+
+Pass named functions to `JSONUIProvider` or a component returned by `createRenderer` with the `functions` prop. These functions resolve `$computed` expressions in element props.
+
+### Custom directives
+
+Pass custom directive definitions to `JSONUIProvider` or a component returned by `createRenderer` with the `directives` prop. Their resolvers can compose with built-in dynamic values such as `$state`:
+
+```tsx
+
+
+
+```
+
## Built-in Actions
The `setState` action is handled automatically by `ActionProvider` and updates the state model directly, which re-evaluates visibility conditions and dynamic prop expressions: