DirkScripts
Script Config (React)
This page covers the React/NUI side of the script config system. For the Lua side (registration, watchers, DB persistence), see dirk_lib â scriptConfig.
#Overview
The script config system lets you build a full admin configuration panel for any FiveM resource with:
- Live settings that apply without server restart
- Undo / redo with full edit history
- Version conflict detection (optimistic concurrency)
- Change audit log with admin attribution
- JSON editor for bulk/advanced edits
- Reset to defaults with confirmation
The architecture flows like this:
schema.json + defaults.json
â
dirk_lib server (registerScriptConfig â MySQL)
â NUI callbacks (get / update / history / reset)
dirk-cfx-react (createScriptConfig â useForm â SettingsPanel)
â React component sections
Admin UI in-game
#Step-by-Step Setup
#1. Define Your Schema
Create schema.json in your resource root. This is a JSON Schema that defines every setting, its type, and its default value.
json{ "type": "object", "properties": { "basic": { "type": "object", "properties": { "debug": { "type": "boolean", "default": false }, "interactionType": { "type": "string", "default": "target" }, "maxPlayers": { "type": "number", "default": 32 } } }, "rewards": { "type": "array", "x-arrayKey": "name", "items": { "type": "object", "properties": { "name": { "type": "string" }, "label": { "type": "string" }, "amount": { "type": "number", "default": 1 } } } } } }
Schema extensions:
| Extension | Purpose |
|---|---|
x-serverOnly | Never sent to client â server-only secrets |
x-renamedFrom | Auto-migrate old key path to new key path |
x-arrayKey | Merge arrays by identity field instead of index |
âšī¸ defaults.json is optional â if provided, it supplies the initial default values. Otherwise defaults are derived from the "default" fields in schema.json.
#2. Register on the Lua Side
See dirk_lib â scriptConfig for full details. The minimal setup:
src/server/scriptConfig.lua
luaCreateThread(function() lib.scriptConfig(schema, function(src) return IsPlayerAceAllowed(src, 'admin') end) end)
src/client/scriptConfig.lua
lualocal _ = lib.scriptConfig -- triggers lazy-load + KVP cache
#3. Create the TypeScript Config Store
In your resource's web/src/stores/, create a file that defines your settings types, Zod schema, and calls createScriptConfig:
tsximport { z } from "zod"; import { createScriptConfig } from "dirk-cfx-react"; // ââ Zod Schema ââââââââââââââââââââââââââââââââââââââââââââââââ const BasicSchema = z.object({ debug: z.boolean(), interactionType: z.string(), maxPlayers: z.number(), }); const RewardSchema = z.object({ name: z.string(), label: z.string(), amount: z.number(), }); export const ScriptConfigSchema = z.object({ basic: BasicSchema, rewards: z.array(RewardSchema), }); export type Settings = z.infer<typeof ScriptConfigSchema>; // ââ Defaults ââââââââââââââââââââââââââââââââââââââââââââââââââ export const defaultScriptConfig: Settings = { basic: { debug: false, interactionType: "target", maxPlayers: 32, }, rewards: [], }; // ââ Store âââââââââââââââââââââââââââââââââââââââââââââââââââââ export const { store: useScriptConfig, updateSettings, getHistory, useScriptConfigHooks, } = createScriptConfig<Settings>(defaultScriptConfig);
#4. Build the Admin Panel
Create web/src/components/Admin/main.tsx with your nav items and section routing:
tsximport { useNuiEvent, SettingsPanel, type NavItem } from "dirk-cfx-react"; import { Settings, Sword } from "lucide-react"; import { useState } from "react"; import { useScriptConfigHooks, defaultScriptConfig, ScriptConfigSchema } from "@/stores/useScriptConfig"; import { BasicSection } from "./BasicSection"; import { RewardsSection } from "./RewardsSection"; const NAV_ITEMS: NavItem[] = [ { id: "basic", icon: Settings, label: "Basic" }, { id: "rewards", icon: Sword, label: "Rewards" }, ]; export function AdminPanel() { const [open, setOpen] = useState(false); useScriptConfigHooks(); useNuiEvent("OPEN_ADMIN_SECTION", () => setOpen(true)); return ( <SettingsPanel navItems={NAV_ITEMS} title="My Resource" subtitle="Admin Settings" open={open} onClose={() => setOpen(false)} defaultScriptConfig={defaultScriptConfig} schema={ScriptConfigSchema} resetConfirmText="my_resource" > {(activeTab) => ( <> {activeTab === "basic" && <BasicSection />} {activeTab === "rewards" && <RewardsSection />} </> )} </SettingsPanel> ); }
#5. Build Section Components
Each admin section uses useFormField and useFormActions from dirk-cfx-react to bind to the form state managed by SettingsPanel:
tsximport { useFormField, useFormActions, AdminPageTitle, InputContainer } from "dirk-cfx-react"; import { Settings } from "lucide-react"; import { Flex, NumberInput, Switch, Select, useMantineTheme } from "@mantine/core"; import type { Settings as SettingsType } from "@/stores/useScriptConfig"; export function BasicSection() { const theme = useMantineTheme(); const color = theme.colors[theme.primaryColor][theme.primaryShade as number]; const basic = useFormField<SettingsType, "basic">("basic"); const { setValue } = useFormActions<SettingsType>(); return ( <Flex direction="column" gap="sm" p="sm"> <AdminPageTitle title="Basic" icon={Settings} color={color} /> <InputContainer title="Debug Mode" description="Enable debug commands and logging"> <Switch checked={basic.debug} onChange={(e) => setValue("basic.debug", e.currentTarget.checked)} /> </InputContainer> <InputContainer title="Interaction Type" description="How players interact with objects"> <Select value={basic.interactionType} onChange={(v) => setValue("basic.interactionType", v)} data={["target", "interact", "textui"]} /> </InputContainer> <InputContainer title="Max Players" description="Maximum concurrent players"> <NumberInput value={basic.maxPlayers} onChange={(v) => setValue("basic.maxPlayers", v)} min={1} max={256} /> </InputContainer> </Flex> ); }
#API Reference
#createScriptConfig<T>(defaultValue)
Factory function that creates a Zustand store and NUI integration for your settings type. Call once at module level.
tsxconst { store, updateSettings, getHistory, useScriptConfigHooks } = createScriptConfig<MySettings>(defaultScriptConfig);
Returns:
| Property | Type | Description |
|---|---|---|
store | StoreApi<T> | Zustand store â use as a hook for reactive reads |
updateSettings | (data: Partial<T>) => Promise<NuiResponse<T>> | Send partial update to server via NUI |
getHistory | (params?) => Promise<HistoryResponse> | Fetch paginated change history |
useScriptConfigHooks | () => void | Call inside your admin component to register NUI listeners |
#SettingsPanel
The main admin UI shell. Renders a full-screen modal with sidebar navigation, undo/redo, save/discard, history, JSON editor, and reset.
tsx<SettingsPanel navItems={NAV_ITEMS} title="My Resource" open={isOpen} defaultScriptConfig={defaults} schema={zodSchema} > {(activeTab) => <>{/* render active section */}</>} </SettingsPanel>
| Prop | Type | Required | Description |
|---|---|---|---|
navItems | NavItem[] | yes | Sidebar navigation items { id, icon, label } |
title | string | yes | Panel title |
subtitle | string | no | Panel subtitle |
open | boolean | yes | Whether the panel is visible |
onClose | () => void | no | Close handler (defaults to fetchNui("CLOSE_ADMIN_SECTION")) |
children | (activeTab: string) => ReactNode | yes | Render function receiving the active tab ID |
defaultScriptConfig | T | yes | Default settings object for reset |
schema | { safeParse } | no | Zod schema for JSON editor validation |
resetConfirmText | string | no | Text user must type to confirm reset |
width | string | no | Panel width (default: "70vh") |
height | string | no | Panel height (default: "85vh") |
Built-in sidebar features:
- Undo / Redo â Step through edit history
- Save â Calls
updateSettingswith all pending changes, shows unsaved count badge - History â Paginated audit log modal with search/filter
- Discard â Revert to last saved state
- Manual Edit (JSON) â Full JSON editor with schema validation
- Reset Defaults â Wipe to defaults with confirmation modal
#useForm
Advanced form state hook with deep nesting, undo/redo, and Zod validation. Used internally by SettingsPanel but can be used standalone.
tsxconst form = useForm<MyFormData>({ initialValues: { ... }, validate: zodSchema, });
Key methods:
| Method | Description |
|---|---|
setValue(path, value) | Set a nested value (e.g. "basic.debug") |
getValue(path) | Get a nested value |
reset() | Reset to initial values |
reinitialize(newValues) | Reset with new initial values |
undo() | Undo last change |
redo() | Redo undone change |
validate() | Run validation, returns { success, errors } |
getInputProps(path) | Returns { value, onChange, error } for Mantine inputs |
isDirty() | Whether any values have changed |
getChangedFields() | Array of paths that have been modified |
#Context Hooks
These hooks access the form state within FormProvider (set up by SettingsPanel):
| Hook | Returns | Description |
|---|---|---|
useFormField(path) | T[path] | Reactive value at a nested path |
useFormFields(...paths) | [T[p1], T[p2], ...] | Reactive values at multiple paths |
useFormActions() | { setValue, reset, ... } | All form mutation methods |
useFormError(path) | string | undefined | Validation error for a field |
useFormErrors() | Record<string, string> | All current validation errors |
#NUI Callback Reference
The settings system communicates with dirk_lib through these NUI callbacks (handled automatically):
| Callback | Direction | Purpose |
|---|---|---|
NUI_READY | UI â Lua | Signals that the NUI is loaded and ready |
GET_SETTINGS | UI â Lua | Fetches server config (currency, colours, etc.) |
GET_LOCALES | UI â Lua | Fetches locale strings |
GET_ITEMS | UI â Lua | Fetches inventory item definitions |
OPEN_ADMIN_SECTION | Lua â UI | Opens the admin panel |
CLOSE_ADMIN_SECTION | UI â Lua | Closes the admin panel |
UPDATE_SCRIPT_SETTINGS | UI â Lua | Sends settings update with expected version |
GET_SCRIPT_SETTINGS_HISTORY | UI â Lua | Fetches paginated change history |
âšī¸ All NUI callbacks are registered automatically by dirk_lib's scriptConfig module. You don't need to set them up manually.
#Version Conflict Handling
When two admins edit settings simultaneously:
- Admin A opens the panel â receives settings +
client_versionhash - Admin B saves a change â server's version hash updates
- Admin A tries to save â sends their
expectedVersion - Server detects mismatch â returns
success: falsewithlatestData - UI automatically merges the latest data back into the form
- Admin A can review and re-save
This prevents silent overwrites and ensures all changes are tracked.
