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.