Skip to content

feat(react-native): support custom directives - #314

Open
karankalsi wants to merge 4 commits into
vercel-labs:mainfrom
karankalsi:feat/react-native-directives
Open

feat(react-native): support custom directives#314
karankalsi wants to merge 4 commits into
vercel-labs:mainfrom
karankalsi:feat/react-native-directives

Conversation

@karankalsi

@karankalsi karankalsi commented Jul 24, 2026

Copy link
Copy Markdown

What

  • add a directives prop to React Native's JSONUIProvider
  • register custom directives once per directives array and expose the registry to prop resolution, including repeat scopes
  • document React Native directive registration in the package README and agent skill

Why

@json-render/react supports custom directives, but @json-render/react-native cannot currently pass a directive registry into resolveElementProps. This brings the React Native renderer to parity so custom $-prefixed dynamic values can compose with built-in expressions such as $state.

Testing

  • pnpm type-check — 59/59 workspace tasks passed on Node 24.12.0 and pnpm 11.1.3
  • pnpm exec prettier --check packages/react-native/src/renderer.tsx
  • manually integrated the packaged change into a dummy screen and verified an $uppercase directive wrapping $state rendered directives are working as DIRECTIVES ARE WORKING

Composed directive example

This parity example registers both directives with defineDirective. Each resolver calls resolvePropValue, so $format can consume the result of $math, while $math resolves its operands from $state:

const mathDirective = 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 formatDirective = 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);
  },
});

<JSONUIProvider directives={[mathDirective, formatDirective]}>
  <Renderer spec={directiveTestSpec} />
</JSONUIProvider>

Mock spec:

{
  "root": "directive-test",
  "state": {
    "price": 12.5,
    "qty": 2
  },
  "elements": {
    "directive-test": {
      "type": "Text",
      "props": {
        "text": {
          "$format": "currency",
          "value": {
            "$math": "multiply",
            "a": { "$state": "/price" },
            "b": { "$state": "/qty" }
          },
          "currency": "USD"
        }
      },
      "children": []
    }
  }
}

With price: 12.5 and qty: 2, the resolved text is $25.00.

Functions and directives parity

React Native now matches @json-render/react for both entry points:

  • JSONUIProvider accepts functions and directives.

  • Components returned by createRenderer accept functions and directives.

  • Both paths provide FunctionsContext and a memoized DirectivesContext registry to element prop resolution, including repeat scopes.

  • Programmatically verified the composed mock resolves to $25.00 with price: 12.5 and qty: 2.

@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

@karankalsi is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant