---
title: 'Next.js'
description: "Learn how to integrate JSONEval-Rs with Next.js using App or Pages routing, and handle server/client environments correctly."
navigation:
  icon: 'i-simple-icons-nextdotjs'
seo:
  title: 'Next.js'
  description: "Learn how to integrate JSONEval-Rs with Next.js using App or Pages routing, and handle server/client environments correctly."
---

# Using JSONEval-Rs in Next.js

Next.js presents a unique challenge for WebAssembly because code might execute on the Server (Node.js backend) or on the Client (Browser/Bundler backend).

This guide shows you how to comprehensively handle both environments.

## 1. Installation

You'll need the core package, a Node backend for SSR, and a client-side WASM target depending on your Next.js bundler.

**For Webpack (Default Next.js Build):**
```bash
yarn add @json-eval-rs/webcore @json-eval-rs/node @json-eval-rs/bundler
```

**For Turbopack (`next dev --turbo`):**
Because Turbopack does not yet support async WebAssembly like Webpack, use the `vanilla` target instead:
```bash
yarn add @json-eval-rs/webcore @json-eval-rs/node @json-eval-rs/vanilla
```

## Built-in BigInt Support

JavaScript natively loses precision for integers extending beyond `Number.MAX_SAFE_INTEGER` (2^53 - 1). JSONEval-Rs utilizes `json-with-bigint` internally across both the Web packages and React Native bindings. This ensures that any deep 64-bit integers (`i64`) returned by the rules engine map seamlessly to JavaScript's native `BigInt` type without precision loss.

## 2. Server-Side Usage (Node.js)

If you are evaluating logic directly inside a Next.js App Router Server Component, Server Action, or a Route Handler (`route.ts`), you are running in a Node.js environment.

You just need to import the `@json-eval-rs/node` backend.

### Standard Validation (Route Handler)

```tsx
// app/api/validate/route.ts
import { JSONEval } from '@json-eval-rs/webcore';
import '@json-eval-rs/node'; // Node backend registers automatically
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const data = await request.json();
  const schema = { /* your schema */ };

  const evalInstance = new JSONEval({ schema: JSON.stringify(schema) });

  try {
    const result = await evalInstance.validate({ data: JSON.stringify(data) });
    return NextResponse.json({ success: !result.has_error, errors: result.errors });
  } finally {
    // Crucial for memory management on the server
    evalInstance.free();
  }
}
```

### Retrieving Evaluated Schema by Path

Often, your server needs to calculate a value or extract specific node states based on the rules engine without returning the entire heavy schema structure.

```tsx
// app/api/calculate/route.ts
import { JSONEval } from '@json-eval-rs/webcore';
import '@json-eval-rs/node';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const data = await request.json();
  const schema = { /* your schema */ };

  const evalInstance = new JSONEval({ schema: JSON.stringify(schema) });

  try {
    await evalInstance.evaluate({ data: JSON.stringify(data) });
    
    // Fetch a specific evaluation result by path
    const totalNode = await evalInstance.getEvaluatedSchemaByPath({ 
      path: "#/properties/total_amount", 
      skipLayout: true 
    });
    
    return NextResponse.json({ total: totalNode.value });
  } finally {
    evalInstance.free();
  }
}
```

## 3. Client-Side Usage (Browser)

When evaluating logic on the browser in Next.js, you must use the `"use client"` directive, dynamically import the WebAssembly module, and prevent SSR from attempting to load the browser module.

### Basic Client Validator

```tsx
// app/components/ClientValidator.tsx
"use client";

import { useEffect, useState } from 'react';
import { JSONEval } from '@json-eval-rs/webcore';

export function ClientValidator() {
  const [evaluator, setEvaluator] = useState<JSONEval | null>(null);
  
  useEffect(() => {
    let instance: JSONEval | null = null;
    let isMounted = true;

    async function load() {
      // Dynamically import the bundler target inside useEffect (browser only)
      // Note: If using Turbopack, import '@json-eval-rs/vanilla' instead.
      const wasmModule = await import('@json-eval-rs/bundler');
      
      if (!isMounted) return;

      instance = new JSONEval({
        schema: JSON.stringify({
          type: "object",
          properties: {
             score: { rules: { minValue: { value: 10, message: "Score too low" } } }
          }
        }),
        wasmModule
      });

      setEvaluator(instance);
    }
    
    load();

    return () => {
      isMounted = false;
      if (instance) instance.free();
    };
  }, []);

  const handleTest = async () => {
    if (!evaluator) return;
    const result = await evaluator.validate({ data: JSON.stringify({ score: 5 }) });
    console.log(result);
  };

  return (
    <div>
      <button onClick={handleTest} disabled={!evaluator}>
        Run Client Validation
      </button>
    </div>
  );
}
```

### Subform Evaluation Usage

For complex UIs like dynamic arrays (e.g., line items, table rows), validating the whole form on every keystroke is expensive. JSONEval-Rs allows you to evaluate specific subforms targeting an isolated context segment.

```tsx
// app/components/OrderForm.tsx
"use client";
import { useState } from 'react';
import { JSONEval } from '@json-eval-rs/webcore';

export function OrderForm() {
  const [evaluator, setEvaluator] = useState<JSONEval | null>(null);

  // ...(Initialization logic similar to above)

  const handleItemChange = async (index: number, itemData: any) => {
    if (!evaluator) return;

    const subformPath = `#/properties/items`;

    // Ensure subform engine boundaries exist
    if (await evaluator.hasSubform(subformPath)) {
      
      // 1. Evaluate only the modified array item
      await evaluator.evaluateSubform({
        subformPath: subformPath,
        data: JSON.stringify(itemData),
        paths: [] // Optional: Array of fields to selectively evaluate within the item
      });

      // 2. Extract specific calculations directly from that subform scope
      const itemCalculation = await evaluator.getEvaluatedSchemaByPathSubform({
        subformPath: subformPath,
        path: "#/properties/item_total" // The property path inside the array item schema
      });

      console.log(`Updated Item Total for row ${index}:`, itemCalculation.value);
    }
  };

  return (
     <div>{ /* Your Form UI */ }</div>
  );
}
```

### Using Web Workers for Non-Blocking UI

When building complex applications, evaluating extraordinarily large schemas on the main thread might cause UI stuttering. Offloading the WebAssembly execution entirely to a Web Worker keeps the React render thread buttery smooth.

Below is a comprehensive architecture utilizing a universal Promise-based message queue via a custom React Hook. Note that Web Workers typically use the `@json-eval-rs/vanilla` target to handle pure WASM initialization gracefully.

**1. Create the Worker File (`app/workers/jsoneval.worker.ts`)**
```typescript
import type { JSONEval } from "@json-eval-rs/vanilla";

let engineInstance: JSONEval | null = null;

self.addEventListener("message", async (event: MessageEvent) => {
  const { id, type, payload } = event.data;

  try {
    if (type === "INIT") {
      const { JSONEval, version } = await import("@json-eval-rs/vanilla");
      
      await JSONEval.initWasm(
        new URL("@json-eval-rs/vanilla/pkg/json_eval_rs_bg.wasm", import.meta.url)
      );

      engineInstance = new JSONEval({
        schema: payload.schema,
        context: payload.context,
        data: payload.data,
      });

      await engineInstance.init();
      self.postMessage({ id, type: "INIT_SUCCESS" });
      return;
    }

    if (!engineInstance) throw new Error("Worker not initialized");

    let result: any;
    switch (type) {
      case "VALIDATE":
        result = await engineInstance.validate(payload);
        self.postMessage({ id, type: "VALIDATE_SUCCESS", result });
        break;
      case "EVALUATE":
        await engineInstance.evaluateOnly(payload);
        result = await engineInstance.getEvaluatedSchemaWithoutParams({ skipLayout: false });
        self.postMessage({ id, type: "EVALUATE_SUCCESS", result });
        break;
      case "EVALUATE_DEPENDENTS":
        result = await engineInstance.evaluateDependents({
          ...payload, reEvaluate: true, includeSubforms: true 
        });
        self.postMessage({ id, type: "EVALUATE_DEPENDENTS_SUCCESS", result });
        break;
      case "GET_EVALUATED_SCHEMA_BY_PATH":
        result = await engineInstance.getEvaluatedSchemaByPath({ path: payload });
        self.postMessage({ id, type: "SUCCESS", result });
        break;
      case "EVALUATE_SUBFORM":
        result = await engineInstance.evaluateSubform(payload);
        self.postMessage({ id, type: "SUCCESS", result });
        break;
      case "GET_EVALUATED_SCHEMA_SUBFORM":
        result = await engineInstance.getEvaluatedSchemaWithoutParamsSubform(payload);
        self.postMessage({ id, type: "SUCCESS", result });
        break;
      case "RELOAD_SCHEMA":
        await engineInstance.reloadSchema(payload);
        self.postMessage({ id, type: "SUCCESS" });
        break;
      case "FREE":
        engineInstance.free();
        engineInstance = null;
        self.postMessage({ id, type: "FREE_SUCCESS" });
        break;
      default:
        throw new Error(`Unknown message type: ${type}`);
    }
  } catch (error: any) {
    self.postMessage({ id, type: "ERROR", error: error.message });
  }
});
```

**2. Create the Promisified React Hook (`hooks/useJSONEvalWorker.ts`)**
```typescript
import { useEffect, useRef, useState, useCallback } from "react";

let messageId = 0;

export function useJSONEvalWorker({ schema, context, data }: any) {
  const workerRef = useRef<Worker | null>(null);
  const pendingRef = useRef<Map<number, { resolve: Function; reject: Function }>>(new Map());

  const [isReady, setIsReady] = useState(false);

  const sendMessage = useCallback((type: string, payload?: any): Promise<any> => {
    return new Promise((resolve, reject) => {
      if (!workerRef.current) return reject(new Error("Worker not initialized"));

      const id = messageId++;
      pendingRef.current.set(id, { resolve, reject });
      workerRef.current.postMessage({ id, type, payload });

      // Optional: Add a timeout cleanup here for orphaned promises
    });
  }, []);

  useEffect(() => {
    const worker = new Worker(new URL("../workers/jsoneval.worker.ts", import.meta.url));
    workerRef.current = worker;

    worker.addEventListener("message", (event) => {
      const { id, type, result, error } = event.data;
      const pending = pendingRef.current.get(id);
      
      if (!pending) return;
      pendingRef.current.delete(id);

      if (type === "ERROR") pending.reject(new Error(error));
      else pending.resolve(result);
    });

    // Initialize the engine inside the worker
    sendMessage("INIT", {
      schema: JSON.stringify(schema),
      context: context ? JSON.stringify(context) : undefined,
      data: data ? JSON.stringify(data) : undefined,
    }).then(() => setIsReady(true)).catch(console.error);

    return () => {
      if (workerRef.current) {
        sendMessage("FREE").catch(console.error);
        workerRef.current.terminate();
      }
    };
  }, [schema, context, data, sendMessage]);

  // Expose the proxy APIs
  const validate = useCallback((data: any) => {
    return sendMessage("VALIDATE", { data: JSON.stringify(data) });
  }, [sendMessage]);

  const evaluateDependents = useCallback((changedPaths: string[], data: any) => {
    return sendMessage("EVALUATE_DEPENDENTS", { changedPaths, data: JSON.stringify(data) });
  }, [sendMessage]);

  const evaluateSubform = useCallback((subformPath: string, itemData: any) => {
    return sendMessage("EVALUATE_SUBFORM", { subformPath, data: JSON.stringify(itemData) });
  }, [sendMessage]);

  return { isReady, validate, evaluateDependents, evaluateSubform /* ... add more as needed */ };
}
```

**3. Use the Hook in your Component**
```tsx
"use client";
import { useJSONEvalWorker } from '@/hooks/useJSONEvalWorker';
import { useState } from 'react';

export function WorkerForm() {
  const { isReady, validate, evaluateDependents } = useJSONEvalWorker({
    schema: { /* your schema */ }
  });

  const handleTest = async () => {
    const result = await validate({ field: "value" });
    console.log("Validation Result from Worker:", result);
  };

  return <button disabled={!isReady} onClick={handleTest}>Validate via Worker</button>;
}
```

## Important Next.js Webpack Config

If you encounter module resolution errors regarding WebAssembly files in your browser console, you might need to enable `asyncWebAssembly` in your `next.config.js` or `next.config.mjs`:

```javascript
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack(config) {
    config.experiments = { ...config.experiments, asyncWebAssembly: true };
    return config;
  },
};

export default nextConfig;
```
