Monospace

useValidatedEvent

Type-safe NUI events with Zod schema validation.

useValidatedEvent extends useEvent with runtime schema validation using Zod. It ensures the data received from the FiveM Lua client matches the expected shape before passing it to your handler.

Import

import { useValidatedEvent } from '@repo/ui/utils/useValidatedEvent'

Signature

function useValidatedEvent<T>(
  eventName: string,
  schema: z.ZodSchema<T>,
  handler: (data: T) => void
): void

Parameters

ParameterTypeDescription
eventNamestringThe NUI message type to listen for
schemaz.ZodSchema<T>Zod schema to validate the incoming data
handler(data: T) => voidCallback invoked with validated data

Usage

import { z } from 'zod'
import { useValidatedEvent } from '@repo/ui/utils/useValidatedEvent'

const PlayerSchema = z.object({
  name: z.string(),
  id: z.number(),
  job: z.string(),
  money: z.number(),
})

function PlayerHUD() {
  const [player, setPlayer] = useState(null)

  useValidatedEvent('updatePlayer', PlayerSchema, (data) => {
    setPlayer(data) // data is fully typed as { name: string, id: number, ... }
  })

  return <div>{player?.name}</div>
}

Corresponding Lua Code

SendNUIMessage({
  type = 'updatePlayer',
  data = {
    name = GetPlayerName(PlayerId()),
    id = GetPlayerServerId(PlayerId()),
    job = 'police',
    money = 50000
  }
})

Validation Errors

When validation fails, the hook logs detailed error information to the browser console instead of calling the handler:

[Validation Failed] Event: "updatePlayer"
-------------------------------------------
[Field: money] -> Expected number, received string
-------------------------------------------

The error tree recursively reports errors for nested objects and arrays, showing the exact field path where validation failed.

Validation failures are silently logged to the console. The handler is not called, and no exception is thrown. This prevents your NUI from crashing due to unexpected data from Lua.

Why Use This?

FiveM's SendNUIMessage sends raw data from Lua to JavaScript. Without validation, a typo or missing field in Lua can cause silent bugs or crashes in your NUI. useValidatedEvent ensures:

  1. Type safety — Your handler receives fully typed data
  2. Runtime validation — Catches schema mismatches at runtime with clear error messages
  3. Safe degradation — Invalid data is logged, not thrown

Source

packages/ui/src/utils/useValidatedEvent.ts
import { treeifyError, z } from 'zod'
import { useEvent } from './useEvent.js'

interface ZodErrorTree {
  errors: string[]
  properties: Record<string, ZodErrorTree>
  items?: ZodErrorTree[]
}

const logErrorTree = (node: ZodErrorTree, path: string = '') => {
  if (node.errors && node.errors.length > 0) {
    node.errors.forEach((msg: string) => {
      console.log(`[Field: ${path || 'root'}] -> ${msg}`)
    })
  }
  if (node.properties) {
    for (const key in node.properties) {
      const currentPath = path ? `${path}.${key}` : key
      if (node.properties[key]) {
        logErrorTree(node.properties[key], currentPath)
      }
    }
  }
  if (node.items && Array.isArray(node.items)) {
    node.items.forEach((item: ZodErrorTree, index: number) => {
      const currentPath = `${path}[${index}]`
      logErrorTree(item, currentPath)
    })
  }
}

export function useValidatedEvent<T>(
  eventName: string,
  schema: z.ZodSchema<T>,
  handler: (data: T) => void
) {
  useEvent<unknown>(eventName, (rawData: unknown) => {
    const result = schema.safeParse(rawData)
    if (result.success) {
      handler(result.data)
    } else {
      const errorResult = treeifyError(result.error) as ZodErrorTree
      console.log(`[Validation Failed] Event: "${eventName}"`)
      console.log('-------------------------------------------')
      if (errorResult.errors && errorResult.errors.length > 0) {
        errorResult.errors.forEach((err) =>
          console.log(`[Root Error]: ${err}`)
        )
      }
      logErrorTree(errorResult)
      console.log('-------------------------------------------')
    }
  })
}

On this page