mdxui

Context Providers

React context providers and hooks for data management

Context Providers

mdxui uses React Context to manage state for collections, records, forms, and resources. These contexts follow the ReactAdmin pattern and provide hooks for accessing and manipulating data.

Overview

mdxui provides four main context types:

  • RecordContext: Single record state
  • ListContext: Collection/list state
  • FormContext: Form editing state
  • ResourceContext: Resource metadata

These contexts are typically provided by view components and consumed by field/input components.

RecordContext

Manages the state for a single record.

Schema

interface RecordContext {
  record?: any              // Current record data
  id?: string | number      // Record ID
  resource: string          // Collection name
  isLoading: boolean        // Initial load state
  isFetching: boolean       // Background refresh state
  error?: any               // Load error
}

Provider

import { RecordContextProvider } from 'mdxui'

<RecordContextProvider value={{
  record: { id: 1, name: 'John Doe', email: 'john@example.com' },
  id: 1,
  resource: 'users',
  isLoading: false,
  isFetching: false
}}>
  <UserDetails />
</RecordContextProvider>

Hook

import { useRecordContext } from 'mdxui'

function UserDetails() {
  const { record, isLoading, error } = useRecordContext()

  if (isLoading) return <Spinner />
  if (error) return <ErrorMessage error={error} />
  if (!record) return <NotFound />

  return (
    <div>
      <h1>{record.name}</h1>
      <p>{record.email}</p>
    </div>
  )
}

Use Cases

  • Show views displaying a single record
  • Edit forms with record data
  • Field components reading record values
  • Detail panels and previews

ListContext

Manages the state for a collection/list of records.

Schema

interface ListContext {
  data?: any[]                      // Record array
  ids?: Array<string | number>      // Record IDs
  total?: number                    // Total count
  isLoading: boolean                // Initial load
  isFetching: boolean               // Background refresh
  error?: any                       // Load error
  page: number                      // Current page (1-indexed)
  perPage: number                   // Items per page
  sort?: SortSpec                   // Current sort
  filterValues?: Record<string, any> // Active filters
  selectedIds?: Array<string | number> // Selected records
  resource: string                  // Collection name
}

Provider

import { ListContextProvider } from 'mdxui'

<ListContextProvider value={{
  data: users,
  ids: users.map(u => u.id),
  total: 42,
  isLoading: false,
  isFetching: false,
  page: 1,
  perPage: 25,
  sort: { field: 'name', order: 'asc' },
  filterValues: { status: 'active' },
  selectedIds: [],
  resource: 'users'
}}>
  <UserList />
</ListContextProvider>

Hook

import { useListContext } from 'mdxui'

function UserList() {
  const {
    data,
    total,
    isLoading,
    page,
    perPage,
    sort,
    filterValues,
    selectedIds
  } = useListContext()

  if (isLoading) return <Spinner />

  return (
    <div>
      <FilterBar filters={filterValues} />
      <DataTable
        data={data}
        sort={sort}
        selectedIds={selectedIds}
      />
      <Pagination
        page={page}
        perPage={perPage}
        total={total}
      />
    </div>
  )
}

Use Cases

  • List views with pagination
  • Data tables with sorting/filtering
  • Kanban boards
  • Calendar views
  • Bulk actions on selected records

FormContext

Manages form editing state with validation.

Schema

interface FormContext {
  values?: Record<string, any>        // Current field values
  initialValues?: Record<string, any> // Original values
  errors?: Record<string, string>     // Validation errors
  touched?: Record<string, boolean>   // Touched fields
  isDirty: boolean                    // Has unsaved changes
  isValid: boolean                    // All validations pass
  isSubmitting: boolean               // Submit in progress
  submitCount: number                 // Number of submit attempts
  resource: string                    // Collection name
}

Provider

import { FormContextProvider } from 'mdxui'

<FormContextProvider value={{
  values: { name: 'John', email: 'john@example.com' },
  initialValues: { name: 'John', email: 'john@example.com' },
  errors: {},
  touched: { name: true },
  isDirty: false,
  isValid: true,
  isSubmitting: false,
  submitCount: 0,
  resource: 'users'
}}>
  <UserForm />
</FormContextProvider>

Hook

import { useFormContext } from 'mdxui'

function UserForm() {
  const {
    values,
    errors,
    touched,
    isDirty,
    isValid,
    isSubmitting
  } = useFormContext()

  return (
    <form>
      <TextInput
        name="name"
        value={values?.name}
        error={touched?.name && errors?.name}
      />
      <TextInput
        name="email"
        value={values?.email}
        error={touched?.email && errors?.email}
      />
      <Button
        type="submit"
        disabled={!isDirty || !isValid || isSubmitting}
      >
        {isSubmitting ? 'Saving...' : 'Save'}
      </Button>
    </form>
  )
}

Use Cases

  • Edit forms with validation
  • Create forms
  • Multi-step wizards
  • Input components reading/writing values
  • Form-level validation and submission

ResourceContext

Provides metadata about a collection/resource.

Schema

interface ResourceContext {
  name: string                    // Resource identifier
  label?: string                  // Human readable name
  labelPlural?: string            // Plural name
  icon?: string                   // Resource icon
  recordRepresentation: string    // Field for record name (default: 'id')
  hasList: boolean                // Has list view (default: true)
  hasShow: boolean                // Has show view (default: true)
  hasEdit: boolean                // Has edit view (default: true)
  hasCreate: boolean              // Has create view (default: true)
}

Provider

import { ResourceContextProvider } from 'mdxui'

<ResourceContextProvider value={{
  name: 'users',
  label: 'User',
  labelPlural: 'Users',
  icon: 'Users',
  recordRepresentation: 'name',
  hasList: true,
  hasShow: true,
  hasEdit: true,
  hasCreate: true
}}>
  <UserViews />
</ResourceContextProvider>

Hook

import { useResourceContext } from 'mdxui'

function ResourceHeader() {
  const {
    label,
    labelPlural,
    icon,
    hasCreate
  } = useResourceContext()

  return (
    <div>
      <Icon name={icon} />
      <h1>{labelPlural}</h1>
      {hasCreate && (
        <Button href="/users/create">
          Create {label}
        </Button>
      )}
    </div>
  )
}

Use Cases

  • Resource-aware navigation
  • CRUD action visibility
  • Breadcrumbs with resource names
  • Dynamic UI based on resource capabilities

DataProvider Context

The DataProvider is injected via context to provide data fetching capabilities.

Schema

interface DataProvider {
  name: string                    // Provider identifier
  type: 'rest' | 'graphql' | 'supabase' | 'firebase' | 'payload' | 'prisma' | 'custom'
  apiUrl?: string                 // API endpoint
  headers?: Record<string, string> // Request headers
  caching?: {
    enabled: boolean              // Enable caching
    ttl: number                   // Cache TTL seconds
    strategy: 'memory' | 'localStorage' | 'indexedDB'
  }
  optimistic: boolean             // Optimistic updates (default: true)
  offline?: {
    enabled: boolean
    syncOnReconnect: boolean
  }
}

Provider

import { DataProviderProvider } from 'mdxui'

<DataProviderProvider value={{
  name: 'payload',
  type: 'payload',
  apiUrl: '/api',
  headers: {
    'Authorization': `Bearer ${token}`
  },
  caching: {
    enabled: true,
    ttl: 300,
    strategy: 'memory'
  },
  optimistic: true
}}>
  <App />
</DataProviderProvider>

Hook

import { useDataProvider } from 'mdxui'

function UserList() {
  const dataProvider = useDataProvider()

  async function loadUsers() {
    const result = await dataProvider.getList('users', {
      pagination: { page: 1, perPage: 25 },
      sort: { field: 'name', order: 'asc' }
    })
    return result
  }

  // ...
}

Combined Context Example

Typical view component providing multiple contexts:

import {
  ResourceContextProvider,
  ListContextProvider,
  useDataProvider
} from 'mdxui'

function UserListView() {
  const dataProvider = useDataProvider()
  const [data, setData] = useState([])
  const [total, setTotal] = useState(0)
  const [isLoading, setIsLoading] = useState(true)
  const [page, setPage] = useState(1)
  const [perPage, setPerPage] = useState(25)

  useEffect(() => {
    async function load() {
      setIsLoading(true)
      const result = await dataProvider.getList('users', {
        pagination: { page, perPage },
        sort: { field: 'name', order: 'asc' }
      })
      setData(result.data)
      setTotal(result.total)
      setIsLoading(false)
    }
    load()
  }, [page, perPage])

  return (
    <ResourceContextProvider value={{
      name: 'users',
      label: 'User',
      labelPlural: 'Users',
      icon: 'Users',
      recordRepresentation: 'name',
      hasList: true,
      hasShow: true,
      hasEdit: true,
      hasCreate: true
    }}>
      <ListContextProvider value={{
        data,
        ids: data.map(u => u.id),
        total,
        isLoading,
        isFetching: false,
        page,
        perPage,
        resource: 'users',
        selectedIds: []
      }}>
        <UserList />
      </ListContextProvider>
    </ResourceContextProvider>
  )
}

Context Utilities

SortSpec

interface SortSpec {
  field: string                   // Field to sort by
  order: 'asc' | 'desc'          // Sort direction
}

Pagination

interface Pagination {
  page: number                    // Current page (1-indexed)
  perPage: number                 // Items per page
  total?: number                  // Total items (from server)
}

FilterCondition

interface FilterCondition {
  field: string                   // Field to filter
  operator: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'contains' | 'startsWith' | 'endsWith' | 'exists' | 'between' | 'regex'
  value: any                      // Filter value
}

Best Practices

1. Use Hooks in Components

Always access context via hooks, not Context.Consumer:

// Good
function UserName() {
  const { record } = useRecordContext()
  return <span>{record?.name}</span>
}

// Avoid
function UserName() {
  return (
    <RecordContext.Consumer>
      {({ record }) => <span>{record?.name}</span>}
    </RecordContext.Consumer>
  )
}

2. Handle Loading States

Always check loading/error states before accessing data:

function UserDetails() {
  const { record, isLoading, error } = useRecordContext()

  if (isLoading) return <Spinner />
  if (error) return <ErrorMessage error={error} />
  if (!record) return <NotFound />

  return <div>{record.name}</div>
}

3. Provide Defaults

Provide sensible defaults for context values:

const defaultListContext: ListContext = {
  data: [],
  ids: [],
  total: 0,
  isLoading: false,
  isFetching: false,
  page: 1,
  perPage: 25,
  resource: '',
  selectedIds: []
}

4. Type Safety

Use TypeScript generics for type-safe contexts:

interface User {
  id: number
  name: string
  email: string
}

function UserCard() {
  const { record } = useRecordContext<User>()
  // record is typed as User | undefined
  return <div>{record?.name}</div>
}

Zod Schemas

All context types have Zod schemas for validation:

import {
  RecordContextSchema,
  ListContextSchema,
  FormContextSchema,
  ResourceContextSchema,
  DataProviderSchema,
} from 'mdxui/zod'

const result = ListContextSchema.safeParse(data)
if (!result.success) {
  console.error(result.error)
}

See Zod Schemas for more details.

On this page