DeenruvDeenruv
Extending the Admin UI

State Management

Zustand-based global stores in @deenruv/react-ui-devkit — settings, server, orders, and more

The @deenruv/react-ui-devkit uses Zustand for global state management. Several stores are provided out of the box for accessing admin panel state such as authentication, channel selection, server configuration, and more.

useSettings

The main persisted settings store for the admin panel. It holds authentication state, UI preferences, and the active channel/language.

import { useSettings } from '@deenruv/react-ui-devkit';

function MyComponent() {
  // Select individual values for optimal re-renders
  const token = useSettings((s) => s.token);
  const selectedChannel = useSettings((s) => s.selectedChannel);
  const translationsLanguage = useSettings((s) => s.translationsLanguage);
  const logIn = useSettings((s) => s.logIn);
  const logOut = useSettings((s) => s.logOut);

  // Check authentication
  if (!token) {
    return <div>Not authenticated</div>;
  }

  return (
    <div>
      <p>Channel: {selectedChannel?.code}</p>
      <p>Language: {translationsLanguage}</p>
      <Button onClick={logOut}>Log Out</Button>
    </div>
  );
}

Key Properties

PropertyTypeDescription
tokenstring | nullCurrent auth Bearer token
selectedChannelChannel | nullCurrently active channel
translationsLanguageLanguageCodeLanguage for entity translations
theme'light' | 'dark'Current UI theme
logIn(credentials) => Promise<void>Log in action
logOut() => voidLog out action

useSettings is persisted — its state survives page reloads. Use Zustand's selector pattern (as shown above) to only subscribe to the specific values your component needs. This prevents unnecessary re-renders.

useServer

Holds server connection state, available channels, permissions, and global configuration:

import { useServer } from '@deenruv/react-ui-devkit';

function ChannelPicker() {
  const channels = useServer((s) => s.channels);
  const permissions = useServer((s) => s.permissions);
  const serverConfig = useServer((s) => s.serverConfig);

  return (
    <div>
      <p>Available channels: {channels.length}</p>
      <p>Custom fields config: {JSON.stringify(serverConfig?.customFieldConfig)}</p>
      {permissions.includes('CreateProduct') && (
        <Button>Create Product</Button>
      )}
    </div>
  );
}

Key Properties

PropertyTypeDescription
channelsChannel[]All available channels
permissionsstring[]Current user's permissions
serverConfigServerConfigServer configuration including custom field definitions

useOrder

Order-specific state management for the order detail view:

import { useOrder } from '@deenruv/react-ui-devkit';

function OrderActions() {
  const order = useOrder((s) => s.order);
  const refetch = useOrder((s) => s.refetch);

  return (
    <div>
      <p>Order #{order?.code} — {order?.state}</p>
      <Button onClick={refetch}>Refresh</Button>
    </div>
  );
}

useGlobalSearch

Global search state for the admin panel search bar:

import { useGlobalSearch } from '@deenruv/react-ui-devkit';

function SearchComponent() {
  const searchTerm = useGlobalSearch((s) => s.searchTerm);
  const setSearchTerm = useGlobalSearch((s) => s.setSearchTerm);
  const results = useGlobalSearch((s) => s.results);

  return (
    <div>
      <Input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      {results.map((result) => (
        <div key={result.id}>{result.name}</div>
      ))}
    </div>
  );
}

GlobalStoreProvider / useGlobalStore

Context-based global store provider for app-wide state. Used by the admin panel internally:

import { useGlobalStore } from '@deenruv/react-ui-devkit';

function MyComponent() {
  const globalState = useGlobalStore();
  // Access app-wide state
}

When to Use Stores vs Plugin Config

Use CaseApproach
Access auth token, language, channeluseSettings
Check user permissionsuseServer
Access server configurationuseServer
Store plugin-specific settingsPlugin config property
Temporary UI stateReact useState or useReducer
Form stateuseDeenruvForm with Zod schema
List/pagination stateuseList

Plugin Config for Custom Settings

If your plugin needs its own configuration, use the config extension point:

import { createDeenruvUIPlugin } from '@deenruv/react-ui-devkit';

interface MyPluginConfig {
  apiEndpoint: string;
  maxItems: number;
  features: {
    enableNotifications: boolean;
    enableWidgets: boolean;
  };
}

export const MyPlugin = createDeenruvUIPlugin<MyPluginConfig>({
  name: 'my-plugin-ui',
  version: '1.0.0',
  config: {
    apiEndpoint: '/api/my-plugin',
    maxItems: 50,
    features: {
      enableNotifications: true,
      enableWidgets: false,
    },
  },
  // ...other extension points
});

Best Practices

Use Selectors for Performance

Always use selector functions with Zustand stores to prevent unnecessary re-renders:

// ✅ Good — only re-renders when token changes
const token = useSettings((s) => s.token);

// ❌ Bad — re-renders on every store update
const settings = useSettings();
const token = settings.token;

Derive State When Possible

Instead of storing derived values, compute them from existing store state:

function MyComponent() {
  const token = useSettings((s) => s.token);
  const isAuthenticated = !!token;   // Derived, not stored

  const permissions = useServer((s) => s.permissions);
  const canCreateProducts = permissions.includes('CreateProduct');  // Derived
}

Combine with React Query Patterns

The Zustand stores work well alongside the useQuery / useMutation hooks for data fetching:

function ProductPage() {
  // Global state from stores
  const channel = useSettings((s) => s.selectedChannel);
  const language = useSettings((s) => s.translationsLanguage);

  // Data fetching with hooks
  const { data, loading } = useQuery(
    (vars) => apiClient('query')({
      product: [{ id: vars.id }, productSelector],
    }),
    { initialVariables: { id: productId } },
  );

  // Form state with useDeenruvForm
  const form = useDeenruvForm({
    schema: z.object({
      name: z.string().min(1, 'Required'),
      slug: z.string(),
    }),
    defaultValues: {
      name: data?.product.name ?? '',
      slug: data?.product.slug ?? '',
    },
  });
}

On this page