DeenruvDeenruv
Extending the Admin UI

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:

APIPurpose
useDeenruvFormCore form hook — wraps RHF with Zod validation
createFormSchemaConvenience helper to build Zod schemas
useZodValidatorsPre-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:

PropertyTypeDescription
setField(field, value) => voidSet a field with auto-validation and dirty marking
hasErrorsbooleanWhether the form currently has any validation errors
isFormValidbooleanWhether the form passes all validation rules
controlControl<T>RHF control — pass to FormField
watch(name?) => T[K]Subscribe to field value changes
handleSubmit(onValid) => (e) => voidForm submission handler
reset(values?) => voidReset form to default or given values
setValue(name, value, options?) => voidLow-level RHF field setter
getValues() => TGet all current field values
formStateFormState<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

ValidatorReturnsDescription
requiredString(msg?)z.ZodStringNon-empty string, defaults to localized "Required"
email(msg?)z.ZodStringValid email, defaults to localized "Invalid email"
positiveNumber(msg?)z.ZodNumberNumber >= 0, defaults to localized "Must be positive"
nonEmptyArray(msg?)z.ZodArrayArray with at least 1 item
translationsWithNamez.ZodArrayArray of translation objects with non-empty name
customFieldsz.ZodOptionalPermissive 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>
PropTypeDescription
formUseDeenruvFormReturn<T>Return value from useDeenruvForm
onSubmit(data: T) => void | Promise<void>Called with validated data on form submit
childrenReactNodeForm content (FormField components, buttons, etc.)
classNamestring?Optional CSS class for the <form> element

Form Primitives

The devkit re-exports shadcn/ui form primitives for building accessible, validated form fields:

ComponentDescription
FormRHF FormProvider — provides form context to children
FormFieldRHF Controller wrapper — connects a field to the form
FormItemField container with spacing
FormLabelAccessible label that turns red on validation errors
FormControlSlot that passes aria-invalid and aria-describedby to the input
FormDescriptionOptional field description text
FormMessageDisplays 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

PropertyTypeDescription
PaginateJSX.ElementPre-built pagination component
objectsT[]Current page items
totalnumberTotal count across all pages
setSort(sort) => voidSet sorting column and direction
setFilter(filter) => voidSet the full filter object
setFilterField(field, value) => voidSet a single filter field
removeFilterField(field) => voidRemove a single filter field
resetFilter() => voidClear all filters
optionInfoobjectCurrent page, perPage, sort, filter, filterOperator
refetch() => voidTrigger a manual refetch
isFilterOnbooleanWhether 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

PropertyTypeDescription
assetsAsset[]Current page of assets
isPendingbooleanWhether a fetch is in progress
errorstring | undefinedError message if fetch failed
totalItemsnumberTotal number of assets
refetchData() => voidTrigger manual refetch
page / setPagenumber / (n) => voidCurrent page
perPage / setPerPagenumber / (n) => voidItems per page
searchTerm / setSearchTermstring / (s) => voidText search query
searchTags / setSearchTagsstring[] / (tags) => voidTag filter
totalPagesnumberCalculated 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();

On this page