A frontend Framework for building admin applications on top of REST services, using ES6, React and Material UI
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
React Admin's data management system provides a powerful abstraction layer for backend communication through data providers and a rich set of hooks for querying and mutating data. Built on React Query, it offers optimistic updates, caching, background refetching, and error handling.
The data provider is the bridge between React Admin and your backend API. It defines a standard interface that React Admin uses for all data operations.
import { DataProvider } from 'react-admin';
interface DataProvider {
getList: (resource: string, params: GetListParams) => Promise<GetListResult>;
getOne: (resource: string, params: GetOneParams) => Promise<GetOneResult>;
getMany: (resource: string, params: GetManyParams) => Promise<GetManyResult>;
getManyReference: (resource: string, params: GetManyReferenceParams) => Promise<GetManyReferenceResult>;
create: (resource: string, params: CreateParams) => Promise<CreateResult>;
update: (resource: string, params: UpdateParams) => Promise<UpdateResult>;
updateMany: (resource: string, params: UpdateManyParams) => Promise<UpdateManyResult>;
delete: (resource: string, params: DeleteParams) => Promise<DeleteResult>;
deleteMany: (resource: string, params: DeleteManyParams) => Promise<DeleteManyResult>;
}interface GetListParams {
pagination: { page: number; perPage: number };
sort: { field: string; order: 'ASC' | 'DESC' };
filter: any;
meta?: any;
}
interface GetOneParams {
id: Identifier;
meta?: any;
}
interface GetManyParams {
ids: Identifier[];
meta?: any;
}
interface GetManyReferenceParams {
target: string;
id: Identifier;
pagination: { page: number; perPage: number };
sort: { field: string; order: 'ASC' | 'DESC' };
filter: any;
meta?: any;
}interface GetListResult {
data: RaRecord[];
total?: number;
pageInfo?: {
hasNextPage?: boolean;
hasPreviousPage?: boolean;
};
}
interface GetOneResult {
data: RaRecord;
}
interface GetManyResult {
data: RaRecord[];
}
interface GetManyReferenceResult {
data: RaRecord[];
total?: number;
pageInfo?: {
hasNextPage?: boolean;
hasPreviousPage?: boolean;
};
}interface CreateParams {
data: Partial<RaRecord>;
meta?: any;
}
interface UpdateParams {
id: Identifier;
data: Partial<RaRecord>;
previousData: RaRecord;
meta?: any;
}
interface UpdateManyParams {
ids: Identifier[];
data: Partial<RaRecord>;
meta?: any;
}
interface DeleteParams {
id: Identifier;
previousData: RaRecord;
meta?: any;
}
interface DeleteManyParams {
ids: Identifier[];
meta?: any;
}interface CreateResult {
data: RaRecord;
}
interface UpdateResult {
data: RaRecord;
}
interface UpdateManyResult {
data?: Identifier[];
}
interface DeleteResult {
data: RaRecord;
}
interface DeleteManyResult {
data?: Identifier[];
}Access the data provider instance directly for custom operations.
import { useDataProvider } from 'react-admin';
const useDataProvider: () => DataProvider;import { useDataProvider } from 'react-admin';
const MyComponent = () => {
const dataProvider = useDataProvider();
const handleCustomOperation = async () => {
try {
const result = await dataProvider.getList('posts', {
pagination: { page: 1, perPage: 10 },
sort: { field: 'title', order: 'ASC' },
filter: { status: 'published' }
});
console.log(result.data);
} catch (error) {
console.error('Error:', error);
}
};
return <button onClick={handleCustomOperation}>Load Posts</button>;
};Fetch a list of records with pagination, sorting, and filtering.
import { useGetList } from 'react-admin';
interface UseGetListOptions {
pagination?: { page: number; perPage: number };
sort?: { field: string; order: 'ASC' | 'DESC' };
filter?: any;
meta?: any;
enabled?: boolean;
staleTime?: number;
refetchInterval?: number;
}
const useGetList: <T extends RaRecord = any>(
resource: string,
options?: UseGetListOptions
) => {
data: T[] | undefined;
total: number | undefined;
pageInfo: PageInfo | undefined;
isLoading: boolean;
isFetching: boolean;
error: any;
refetch: () => void;
};import { useGetList } from 'react-admin';
const PostList = () => {
const {
data: posts,
total,
isLoading,
error,
refetch
} = useGetList('posts', {
pagination: { page: 1, perPage: 10 },
sort: { field: 'publishedAt', order: 'DESC' },
filter: { status: 'published' }
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<button onClick={() => refetch()}>Refresh</button>
<p>Total: {total} posts</p>
{posts?.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
};Fetch a single record by ID.
import { useGetOne } from 'react-admin';
interface GetOneParams {
id: Identifier;
meta?: any;
}
interface UseGetOneOptions {
enabled?: boolean;
staleTime?: number;
refetchInterval?: number;
}
const useGetOne: <T extends RaRecord = any>(
resource: string,
params: GetOneParams,
options?: UseGetOneOptions
) => {
data: T | undefined;
isLoading: boolean;
isFetching: boolean;
isSuccess: boolean;
isError: boolean;
error: any;
refetch: () => void;
status: 'idle' | 'loading' | 'error' | 'success';
};import { useGetOne } from 'react-admin';
const PostDetail = ({ id }: { id: string }) => {
const { data: post, isLoading, error } = useGetOne('posts', { id });
if (isLoading) return <div>Loading post...</div>;
if (error) return <div>Error loading post</div>;
if (!post) return <div>Post not found</div>;
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
};Fetch multiple records by their IDs.
import { useGetMany } from 'react-admin';
interface UseGetManyOptions {
ids: Identifier[];
meta?: any;
enabled?: boolean;
}
const useGetMany: <T extends RaRecord = any>(
resource: string,
options: UseGetManyOptions
) => {
data: T[] | undefined;
isLoading: boolean;
isFetching: boolean;
error: any;
refetch: () => void;
};Fetch multiple records by their IDs with batching and deduplication. When multiple components call this hook with overlapping IDs in the same tick, it aggregates all requests into a single getMany call.
import { useGetManyAggregate } from 'react-admin';
interface UseGetManyAggregateOptions {
ids: Identifier[];
meta?: any;
enabled?: boolean;
}
const useGetManyAggregate: <T extends RaRecord = any>(
resource: string,
options: UseGetManyAggregateOptions
) => {
data: T[] | undefined;
isLoading: boolean;
isFetching: boolean;
error: any;
refetch: () => void;
};import { useGetManyAggregate } from 'react-admin';
// If multiple components call this with overlapping IDs:
// Component A: useGetManyAggregate('tags', [1,2,3])
// Component B: useGetManyAggregate('tags', [3,4,5])
// Result: Single call to dataProvider.getMany('tags', [1,2,3,4,5])
const TagDisplay = ({ tagIds }: { tagIds: number[] }) => {
const { data: tags, isLoading } = useGetManyAggregate('tags', { ids: tagIds });
if (isLoading) return <div>Loading tags...</div>;
return (
<div>
{tags?.map(tag => (
<span key={tag.id} className="tag">{tag.name}</span>
))}
</div>
);
};Fetch records that reference another record.
import { useGetManyReference } from 'react-admin';
interface UseGetManyReferenceOptions {
target: string;
id: Identifier;
pagination?: { page: number; perPage: number };
sort?: { field: string; order: 'ASC' | 'DESC' };
filter?: any;
meta?: any;
}
const useGetManyReference: <T extends RaRecord = any>(
resource: string,
options: UseGetManyReferenceOptions
) => {
data: T[] | undefined;
total: number | undefined;
pageInfo: PageInfo | undefined;
isLoading: boolean;
error: any;
refetch: () => void;
};import { useGetManyReference } from 'react-admin';
const UserComments = ({ userId }: { userId: string }) => {
const {
data: comments,
total,
isLoading
} = useGetManyReference('comments', {
target: 'userId',
id: userId,
sort: { field: 'createdAt', order: 'DESC' }
});
if (isLoading) return <div>Loading comments...</div>;
return (
<div>
<h3>{total} Comments</h3>
{comments?.map(comment => (
<div key={comment.id}>{comment.text}</div>
))}
</div>
);
};Fetch paginated data with infinite scrolling support.
import { useInfiniteGetList } from 'react-admin';
const useInfiniteGetList: <T extends RaRecord = any>(
resource: string,
options?: UseGetListOptions
) => {
data: T[] | undefined;
total: number | undefined;
isLoading: boolean;
isFetching: boolean;
hasNextPage: boolean;
fetchNextPage: () => void;
error: any;
};Create a new record.
import { useCreate } from 'react-admin';
interface UseCreateOptions {
mutationMode?: 'pessimistic' | 'optimistic' | 'undoable';
returnPromise?: boolean;
onSuccess?: (data: any, variables: any, context: any) => void;
onError?: (error: any, variables: any, context: any) => void;
meta?: any;
}
const useCreate: <T extends RaRecord = any>(
resource?: string,
options?: UseCreateOptions
) => [
create: (resource?: string, params?: CreateParams, options?: UseCreateOptions) => Promise<T> | void,
{ data: T | undefined; isLoading: boolean; error: any }
];import { useCreate, useNotify, useRedirect } from 'react-admin';
const CreatePostButton = () => {
const notify = useNotify();
const redirect = useRedirect();
const [create, { isLoading }] = useCreate('posts', {
onSuccess: (data) => {
notify('Post created successfully');
redirect('show', 'posts', data.id);
},
onError: (error) => {
notify('Error creating post', { type: 'error' });
}
});
const handleCreate = () => {
create('posts', {
data: {
title: 'New Post',
content: 'Post content...',
status: 'draft'
}
});
};
return (
<button onClick={handleCreate} disabled={isLoading}>
{isLoading ? 'Creating...' : 'Create Post'}
</button>
);
};Update an existing record.
import { useUpdate } from 'react-admin';
interface UseUpdateOptions {
mutationMode?: 'pessimistic' | 'optimistic' | 'undoable';
returnPromise?: boolean;
onSuccess?: (data: any, variables: any, context: any) => void;
onError?: (error: any, variables: any, context: any) => void;
meta?: any;
}
const useUpdate: <T extends RaRecord = any>(
resource?: string,
options?: UseUpdateOptions
) => [
update: (resource?: string, params?: UpdateParams, options?: UseUpdateOptions) => Promise<T> | void,
{ data: T | undefined; isLoading: boolean; error: any }
];Delete a record.
import { useDelete } from 'react-admin';
interface UseDeleteOptions {
mutationMode?: 'pessimistic' | 'optimistic' | 'undoable';
returnPromise?: boolean;
onSuccess?: (data: any, variables: any, context: any) => void;
onError?: (error: any, variables: any, context: any) => void;
meta?: any;
}
const useDelete: <T extends RaRecord = any>(
resource?: string,
options?: UseDeleteOptions
) => [
deleteOne: (resource?: string, params?: DeleteParams, options?: UseDeleteOptions) => Promise<T> | void,
{ data: T | undefined; isLoading: boolean; error: any }
];Bulk operations for updating or deleting multiple records.
import { useUpdateMany, useDeleteMany } from 'react-admin';
const useUpdateMany: <T extends RaRecord = any>(
resource?: string,
options?: UseMutationOptions
) => [
updateMany: (resource?: string, params?: UpdateManyParams, options?: UseMutationOptions) => Promise<T> | void,
MutationResult
];
const useDeleteMany: <T extends RaRecord = any>(
resource?: string,
options?: UseMutationOptions
) => [
deleteMany: (resource?: string, params?: DeleteManyParams, options?: UseMutationOptions) => Promise<T> | void,
MutationResult
];Refresh all queries and reload data.
import { useRefresh } from 'react-admin';
const useRefresh: () => () => void;import { useRefresh } from 'react-admin';
const RefreshButton = () => {
const refresh = useRefresh();
return (
<button onClick={refresh}>
Refresh All Data
</button>
);
};Access loading state across the application.
import { useLoading } from 'react-admin';
const useLoading: () => boolean;Extract record ID from the current route.
import { useGetRecordId } from 'react-admin';
const useGetRecordId: () => Identifier | undefined;Check if initial data has been loaded.
import { useIsDataLoaded } from 'react-admin';
const useIsDataLoaded: () => boolean;Combine multiple data providers for different resources or namespaces.
import { combineDataProviders } from 'react-admin';
const combineDataProviders: (dataProviderMatcher: DataProviderMatcher) => DataProvider;
type DataProviderMatcher = (resource: string) => DataProvider;import { combineDataProviders } from 'react-admin';
import jsonServerProvider from 'ra-data-json-server';
import graphqlProvider from 'ra-data-graphql';
const postsProvider = jsonServerProvider('http://localhost:3001');
const usersProvider = graphqlProvider({ uri: 'http://localhost:4000/graphql' });
const dataProvider = combineDataProviders((resource: string) => {
switch (resource) {
case 'posts':
case 'comments':
return postsProvider;
case 'users':
case 'profiles':
return usersProvider;
default:
return postsProvider; // fallback
}
});Add lifecycle callbacks to data provider methods.
import { withLifecycleCallbacks } from 'react-admin';
interface ResourceCallbacks<T extends RaRecord = any> {
resource: string;
// Read callbacks
beforeGetList?: (params: GetListParams, dataProvider: DataProvider) => Promise<GetListParams>;
afterGetList?: (result: GetListResult<T>, dataProvider: DataProvider) => Promise<GetListResult<T>>;
beforeGetOne?: (params: GetOneParams, dataProvider: DataProvider) => Promise<GetOneParams>;
afterGetOne?: (result: GetOneResult<T>, dataProvider: DataProvider) => Promise<GetOneResult<T>>;
beforeGetMany?: (params: GetManyParams, dataProvider: DataProvider) => Promise<GetManyParams>;
afterGetMany?: (result: GetManyResult<T>, dataProvider: DataProvider) => Promise<GetManyResult<T>>;
beforeGetManyReference?: (params: GetManyReferenceParams, dataProvider: DataProvider) => Promise<GetManyReferenceParams>;
afterGetManyReference?: (result: GetManyReferenceResult<T>, dataProvider: DataProvider) => Promise<GetManyReferenceResult<T>>;
// Mutation callbacks
beforeCreate?: (params: CreateParams, dataProvider: DataProvider) => Promise<CreateParams>;
afterCreate?: (result: CreateResult<T>, dataProvider: DataProvider) => Promise<CreateResult<T>>;
beforeUpdate?: (params: UpdateParams, dataProvider: DataProvider) => Promise<UpdateParams>;
afterUpdate?: (result: UpdateResult<T>, dataProvider: DataProvider) => Promise<UpdateResult<T>>;
beforeUpdateMany?: (params: UpdateManyParams, dataProvider: DataProvider) => Promise<UpdateManyParams>;
afterUpdateMany?: (result: UpdateManyResult, dataProvider: DataProvider) => Promise<UpdateManyResult>;
beforeDelete?: (params: DeleteParams, dataProvider: DataProvider) => Promise<DeleteParams>;
afterDelete?: (result: DeleteResult<T>, dataProvider: DataProvider) => Promise<DeleteResult<T>>;
beforeDeleteMany?: (params: DeleteManyParams, dataProvider: DataProvider) => Promise<DeleteManyParams>;
afterDeleteMany?: (result: DeleteManyResult, dataProvider: DataProvider) => Promise<DeleteManyResult>;
// Generic callbacks
beforeSave?: (data: Partial<T>, dataProvider: DataProvider) => Promise<Partial<T>>;
afterSave?: (record: T, dataProvider: DataProvider) => Promise<T>;
afterRead?: (record: T, dataProvider: DataProvider) => Promise<T>;
}
const withLifecycleCallbacks: (
dataProvider: DataProvider,
callbacks: ResourceCallbacks[]
) => DataProvider;import { withLifecycleCallbacks } from 'react-admin';
const dataProviderWithCallbacks = withLifecycleCallbacks(baseDataProvider, [
{
resource: 'posts',
beforeCreate: async (params, dataProvider) => {
// Add timestamp before creating
return {
...params,
data: { ...params.data, createdAt: new Date().toISOString() }
};
},
afterCreate: async (result, dataProvider) => {
// Log creation
console.log('Post created:', result.data);
return result;
},
beforeSave: async (data, dataProvider) => {
// Generic save callback (applies to create/update)
return { ...data, updatedAt: new Date().toISOString() };
}
},
{
resource: 'users',
afterRead: async (record, dataProvider) => {
// Transform user data after reading
return { ...record, fullName: `${record.firstName} ${record.lastName}` };
}
}
]);Custom error class for HTTP-related errors.
import { HttpError } from 'react-admin';
class HttpError extends Error {
status: number;
body?: any;
constructor(message: string, status: number, body?: any);
}React Admin provides several additional utilities for working with data providers:
import {
DataProviderContext,
testDataProvider,
fetchUtils,
undoableEventEmitter,
convertLegacyDataProvider
} from 'react-admin';
// Context for accessing data provider
const DataProviderContext: React.Context<DataProvider>;
// Test data provider for development/testing
const testDataProvider: DataProvider;
// Fetch utilities for HTTP requests
const fetchUtils: {
fetchJson: (url: string, options?: any) => Promise<{ status: number; headers: Headers; body: any; json: any }>;
queryParameters: (data: any) => string;
};
// Event emitter for undoable operations
const undoableEventEmitter: {
emit: (event: string, ...args: any[]) => void;
on: (event: string, listener: (...args: any[]) => void) => void;
off: (event: string, listener: (...args: any[]) => void) => void;
};
// Convert legacy data provider format to current
const convertLegacyDataProvider: (legacyDataProvider: any) => DataProvider;import { fetchUtils } from 'react-admin';
const httpClient = (url: string, options: any = {}) => {
options.headers = new Headers({
Accept: 'application/json',
Authorization: `Bearer ${localStorage.getItem('token')}`,
...options.headers,
});
return fetchUtils.fetchJson(url, options);
};
// Use with data provider
const dataProvider = jsonServerProvider('http://localhost:3001', httpClient);import { DataProvider, HttpError } from 'react-admin';
const customDataProvider: DataProvider = {
getList: async (resource, params) => {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const query = new URLSearchParams({
_page: page.toString(),
_limit: perPage.toString(),
_sort: field,
_order: order.toLowerCase(),
...params.filter,
});
const response = await fetch(`${apiUrl}/${resource}?${query}`);
if (!response.ok) {
throw new HttpError('Network error', response.status);
}
const data = await response.json();
const total = parseInt(response.headers.get('X-Total-Count') || '0');
return { data, total };
},
getOne: async (resource, params) => {
const response = await fetch(`${apiUrl}/${resource}/${params.id}`);
if (!response.ok) {
throw new HttpError('Record not found', response.status);
}
const data = await response.json();
return { data };
},
// ... implement other methods
};import { useUpdate, useNotify, useGetOne } from 'react-admin';
const OptimisticUpdateExample = ({ id }: { id: string }) => {
const notify = useNotify();
const { data: post } = useGetOne('posts', { id });
const [update, { isLoading }] = useUpdate('posts', {
mutationMode: 'optimistic', // Updates UI immediately
onSuccess: () => {
notify('Post updated successfully');
},
onError: (error) => {
notify('Update failed', { type: 'error' });
}
});
const handleTogglePublished = () => {
if (!post) return;
update('posts', {
id: post.id,
data: { published: !post.published },
previousData: post
});
};
return (
<button onClick={handleTogglePublished} disabled={isLoading}>
{post?.published ? 'Unpublish' : 'Publish'}
</button>
);
};React Admin's data management system provides a robust foundation for handling all backend interactions while maintaining excellent user experience through optimistic updates, intelligent caching, and comprehensive error handling.
Install with Tessl CLI
npx tessl i tessl/npm-react-admin