Hooks
All hooks provided by @deenruv/react-ui-devkit — form management, lists, translations, assets, and more
The @deenruv/react-ui-devkit package provides a set of purpose-built React hooks for building admin UI plugins. These hooks handle common patterns like form state, paginated lists, translations, and asset management.
Form Management
Deenruv uses React Hook Form + Zod for form state and validation. The devkit provides three complementary APIs:
| API | Purpose |
|---|---|
useDeenruvForm | Core form hook — wraps RHF with Zod validation |
createFormSchema | Convenience helper to build Zod schemas |
useZodValidators | Pre-built, i18n-aware Zod validators |
These work together with the Form primitives (Form, FormField, FormItem, FormLabel, FormControl, FormMessage) to build fully accessible, validated forms.
useDeenruvForm — Core Form Hook
The standard way to create form state in Deenruv admin plugins. It wraps React Hook Form's useForm with Zod schema validation, providing a convenient setField helper and validity flags.
Signature
function useDeenruvForm<T extends FieldValues>(
options: UseDeenruvFormOptions<T>,
): UseDeenruvFormReturn<T>;
interface UseDeenruvFormOptions<T extends FieldValues> {
/** Zod schema for validation — pass z.object({...}) or any concrete Zod type. */
schema: ZodSchema<T>;
defaultValues?: UseFormProps<T>['defaultValues'];
mode?: UseFormProps<T>['mode']; // default: 'onTouched'
}Basic Usage
import { useDeenruvForm, z } from '@deenruv/react-ui-devkit';
const schema = z.object({
name: z.string().min(1, 'Name is required'),
slug: z.string().min(1, 'Slug is required'),
description: z.string(),
enabled: z.boolean(),
});
const form = useDeenruvForm({
schema,
defaultValues: { name: '', slug: '', description: '', enabled: true },
});Accessing and Updating State
// Watch a field value (reactive)
const name = form.watch('name');
// Set a single field (auto-validates + marks dirty)
form.setField('name', 'New Product');
// Use the full RHF API
form.setValue('slug', 'new-product');
form.reset({ name: '', slug: '', description: '', enabled: true });Using with Form Primitives
import {
useDeenruvForm, z,
DeenruvForm,
FormField, FormItem, FormLabel, FormControl, FormMessage,
Input, Button,
} from '@deenruv/react-ui-devkit';
const schema = z.object({
name: z.string().min(1, 'Name is required'),
slug: z.string(),
});
function ProductForm() {
const form = useDeenruvForm({
schema,
defaultValues: { name: '', slug: '' },
});
const onSubmit = async (data: z.infer<typeof schema>) => {
await apiClient('mutation')({
updateProduct: [{ input: { id: productId, ...data } }, { id: true }],
});
};
return (
<DeenruvForm form={form} onSubmit={onSubmit}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Product name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Save</Button>
</DeenruvForm>
);
}DeenruvForm wraps the shadcn/ui Form provider and <form> element. It passes the entire useDeenruvForm return into the RHF context, so FormField, FormMessage, etc. work automatically.
Using with DetailView
import {
DetailView, useDeenruvForm, z,
DeenruvForm,
FormField, FormItem, FormLabel, FormControl, FormMessage,
Input, Button, Switch, PageBlock, RichTextEditor, useTranslation,
} from '@deenruv/react-ui-devkit';
const productSchema = z.object({
name: z.string().min(1, 'Required'),
slug: z.string(),
description: z.string(),
enabled: z.boolean(),
});
type ProductFormValues = z.infer<typeof productSchema>;
function ProductDetailPage() {
const { id } = useParams();
const { t } = useTranslation('catalog');
const form = useDeenruvForm({
schema: productSchema,
defaultValues: { name: '', slug: '', description: '', enabled: true },
});
const handleSave = async (data: ProductFormValues) => {
await apiClient('mutation')({
updateProduct: [
{
input: {
id,
enabled: data.enabled,
translations: [{
languageCode: LanguageCode.en,
name: data.name,
slug: data.slug,
description: data.description,
}],
},
},
{ id: true },
],
});
};
return (
<DeenruvForm form={form} onSubmit={handleSave}>
<DetailView
title={form.watch('name') || t('product.new')}
locationId="products-detail-view"
tabs={[
{
id: 'product',
label: t('tabs.product'),
content: (
<PageBlock title={t('product.basicInfo')}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('product.name')}</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
render={({ field }) => (
<FormItem>
<FormLabel>{t('product.slug')}</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>{t('product.description')}</FormLabel>
<FormControl>
<RichTextEditor value={field.value} onChange={field.onChange} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</PageBlock>
),
},
]}
sidebar={
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem>
<FormLabel>{t('product.enabled')}</FormLabel>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
}
actions={<Button type="submit">{t('common.save')}</Button>}
/>
</DeenruvForm>
);
}Return Value Reference
useDeenruvForm returns everything from React Hook Form's useForm, plus:
| Property | Type | Description |
|---|---|---|
setField | (field, value) => void | Set a field with auto-validation and dirty marking |
hasErrors | boolean | Whether the form currently has any validation errors |
isFormValid | boolean | Whether the form passes all validation rules |
control | Control<T> | RHF control — pass to FormField |
watch | (name?) => T[K] | Subscribe to field value changes |
handleSubmit | (onValid) => (e) => void | Form submission handler |
reset | (values?) => void | Reset form to default or given values |
setValue | (name, value, options?) => void | Low-level RHF field setter |
getValues | () => T | Get all current field values |
formState | FormState<T> | Full RHF form state (errors, isDirty, isSubmitting, etc.) |
createFormSchema — Schema Builder
A convenience wrapper around z.object() for consistent schema creation:
import { createFormSchema } from '@deenruv/react-ui-devkit';
import { z } from 'zod';
const schema = createFormSchema({
name: z.string().min(1, 'Name is required'),
code: z.string().min(1, 'Code is required'),
price: z.number().min(0),
customFields: z.record(z.unknown()).optional(),
});This is equivalent to calling z.object(...) directly but makes the intent clear and provides a standard pattern for admin form schemas.
useZodValidators — Pre-built Validators
Provides i18n-aware Zod schemas for common validation patterns. Error messages are automatically localized using the admin panel's translation system.
import { useZodValidators, createFormSchema, useDeenruvForm } from '@deenruv/react-ui-devkit';
function MyForm() {
const { requiredString, email, positiveNumber, customFields } = useZodValidators();
const schema = createFormSchema({
name: requiredString(), // z.string().min(1, t('validation.required'))
emailAddress: email(), // z.string().email(t('validation.invalidEmail'))
price: positiveNumber(), // z.number().min(0, t('validation.mustBePositive'))
customFields, // z.record(z.unknown()).optional().default({})
});
const form = useDeenruvForm({
schema,
defaultValues: { name: '', emailAddress: '', price: 0, customFields: {} },
});
// ...
}Available Validators
| Validator | Returns | Description |
|---|---|---|
requiredString(msg?) | z.ZodString | Non-empty string, defaults to localized "Required" |
email(msg?) | z.ZodString | Valid email, defaults to localized "Invalid email" |
positiveNumber(msg?) | z.ZodNumber | Number >= 0, defaults to localized "Must be positive" |
nonEmptyArray(msg?) | z.ZodArray | Array with at least 1 item |
translationsWithName | z.ZodArray | Array of translation objects with non-empty name |
customFields | z.ZodOptional | Permissive record for custom fields, defaults to {} |
DeenruvForm — Form Wrapper Component
A wrapper component that provides form context and handles submission:
import { DeenruvForm } from '@deenruv/react-ui-devkit';
<DeenruvForm form={form} onSubmit={handleSubmit} className="space-y-4">
{/* FormField components go here */}
</DeenruvForm>| Prop | Type | Description |
|---|---|---|
form | UseDeenruvFormReturn<T> | Return value from useDeenruvForm |
onSubmit | (data: T) => void | Promise<void> | Called with validated data on form submit |
children | ReactNode | Form content (FormField components, buttons, etc.) |
className | string? | Optional CSS class for the <form> element |
Form Primitives
The devkit re-exports shadcn/ui form primitives for building accessible, validated form fields:
| Component | Description |
|---|---|
Form | RHF FormProvider — provides form context to children |
FormField | RHF Controller wrapper — connects a field to the form |
FormItem | Field container with spacing |
FormLabel | Accessible label that turns red on validation errors |
FormControl | Slot that passes aria-invalid and aria-describedby to the input |
FormDescription | Optional field description text |
FormMessage | Displays the field's validation error message |
Standard Pattern
<FormField
control={form.control}
name="fieldName"
render={({ field }) => (
<FormItem>
<FormLabel>Label</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>Helper text</FormDescription>
<FormMessage />
</FormItem>
)}
/>Accessibility: When a field has validation errors, FormControl sets aria-invalid="true" on the input and links it to the FormMessage via aria-describedby. Screen readers announce errors automatically.
useList — Paginated Lists
Manages paginated list state with URL search params for sorting, filtering, and pagination. Returns a pre-built Paginate JSX element.
Basic Usage
import { useList, apiClient } from '@deenruv/react-ui-devkit';
const {
Paginate, // Pre-built pagination JSX component
objects, // Current page items
total, // Total item count
setSort, // Set sort column
setFilter, // Set filter object
setFilterField, // Set individual filter field
removeFilterField, // Remove individual filter field
resetFilter, // Clear all filters
optionInfo, // Current { page, perPage, sort, filter, filterOperator }
refetch, // Manual refetch
isFilterOn, // Whether any filters are active
} = useList({
route: (options) =>
apiClient('query')({
products: [
{
options: {
take: options.perPage,
skip: (options.page - 1) * options.perPage,
sort: options.sort
? { [options.sort.key]: options.sort.sortDir }
: undefined,
filter: options.filter,
},
},
{ totalItems: true, items: { id: true, name: true, slug: true } },
],
}).then((r) => r.products),
listType: 'products',
});Using with DetailList Template
The useList hook is used internally by the DetailList template component. If you're building a standard list page, prefer DetailList for a higher-level API. Use useList directly when you need full control over the list layout.
Return Value Reference
| Property | Type | Description |
|---|---|---|
Paginate | JSX.Element | Pre-built pagination component |
objects | T[] | Current page items |
total | number | Total count across all pages |
setSort | (sort) => void | Set sorting column and direction |
setFilter | (filter) => void | Set the full filter object |
setFilterField | (field, value) => void | Set a single filter field |
removeFilterField | (field) => void | Remove a single filter field |
resetFilter | () => void | Clear all filters |
optionInfo | object | Current page, perPage, sort, filter, filterOperator |
refetch | () => void | Trigger a manual refetch |
isFilterOn | boolean | Whether any filter is active |
useTranslation — Internationalization
Never import react-i18next directly. Always use useTranslation from @deenruv/react-ui-devkit. This hook binds to the global Deenruv i18n instance via window.__DEENRUV_SETTINGS__.i18n, ensuring your translations work correctly with the admin panel's language system.
Basic Usage
import { useTranslation } from '@deenruv/react-ui-devkit';
const { t, tEntity, i18n } = useTranslation('my-plugin-namespace');
// Standard translation
t('my.key'); // "My translated string"
// Entity-aware translation with pluralization
tEntity('entity.title', 'Product', 'one'); // "Product"
tEntity('entity.title', 'Product', 'many'); // "Products"
tEntity('entity.title', 'Product', 5); // "5 Products"In Components
function MyComponent() {
const { t } = useTranslation('reviews');
return (
<div>
<h1>{t('page.title')}</h1>
<p>{t('page.description')}</p>
</div>
);
}useAssets — Asset Management
Manages asset browsing with pagination, text search, and tag filtering:
import { useAssets } from '@deenruv/react-ui-devkit';
const {
assets, // Current page of assets
isPending, // Loading state
error, // Error message
totalItems, // Total asset count
refetchData, // Manual refetch
page, setPage,
perPage, setPerPage,
searchTerm, setSearchTerm,
searchTags, setSearchTags,
totalPages,
} = useAssets();Return Value Reference
| Property | Type | Description |
|---|---|---|
assets | Asset[] | Current page of assets |
isPending | boolean | Whether a fetch is in progress |
error | string | undefined | Error message if fetch failed |
totalItems | number | Total number of assets |
refetchData | () => void | Trigger manual refetch |
page / setPage | number / (n) => void | Current page |
perPage / setPerPage | number / (n) => void | Items per page |
searchTerm / setSearchTerm | string / (s) => void | Text search query |
searchTags / setSearchTags | string[] / (tags) => void | Tag filter |
totalPages | number | Calculated total pages |
useDebounce
Re-exported from the use-debounce package. Debounces a value with a configurable delay:
import { useDebounce } from '@deenruv/react-ui-devkit';
const [debouncedSearch] = useDebounce(searchTerm, 300);useLocalStorage
Persistent state management using browser localStorage:
import { useLocalStorage } from '@deenruv/react-ui-devkit';
const [viewMode, setViewMode] = useLocalStorage('plugin-view-mode', 'grid');useErrorHandler
Centralized error handling for GraphQL and API errors:
import { useErrorHandler } from '@deenruv/react-ui-devkit';
const { handleError } = useErrorHandler();
try {
await apiClient('mutation')({ /* ... */ });
} catch (error) {
handleError(error);
}useCustomSearchParams
Helper for managing URL search parameters in list views. Used internally by useList:
import { useCustomSearchParams } from '@deenruv/react-ui-devkit';
const { searchParams, setSearchParam } = useCustomSearchParams();