CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-admin

A frontend Framework for building admin applications on top of REST services, using ES6, React and Material UI

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

data-management.mddocs/

Data Management

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.

Data Provider Interface

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>;
}

Query Parameter Types

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;
}

Result Types

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;
  };
}

Mutation Parameter Types

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;
}

Mutation Result Types

interface CreateResult {
  data: RaRecord;
}

interface UpdateResult {
  data: RaRecord;
}

interface UpdateManyResult {
  data?: Identifier[];
}

interface DeleteResult {
  data: RaRecord;
}

interface DeleteManyResult {
  data?: Identifier[];
}

Data Provider Hooks

useDataProvider

Access the data provider instance directly for custom operations.

import { useDataProvider } from 'react-admin';

const useDataProvider: () => DataProvider;

Usage Example

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>;
};

Query Hooks

useGetList

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;
};

Usage Example

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>
  );
};

useGetOne

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';
};

Usage Example

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>
  );
};

useGetMany

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;
};

useGetManyAggregate

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;
};

Usage Example

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>
  );
};

useGetManyReference

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;
};

Usage Example

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>
  );
};

useInfiniteGetList

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;
};

Mutation Hooks

useCreate

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 }
];

Usage Example

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>
  );
};

useUpdate

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 }
];

useDelete

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 }
];

useUpdateMany & useDeleteMany

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
];

Utility Hooks

useRefresh

Refresh all queries and reload data.

import { useRefresh } from 'react-admin';

const useRefresh: () => () => void;

Usage Example

import { useRefresh } from 'react-admin';

const RefreshButton = () => {
  const refresh = useRefresh();
  
  return (
    <button onClick={refresh}>
      Refresh All Data
    </button>
  );
};

useLoading

Access loading state across the application.

import { useLoading } from 'react-admin';

const useLoading: () => boolean;

useGetRecordId

Extract record ID from the current route.

import { useGetRecordId } from 'react-admin';

const useGetRecordId: () => Identifier | undefined;

useIsDataLoaded

Check if initial data has been loaded.

import { useIsDataLoaded } from 'react-admin';

const useIsDataLoaded: () => boolean;

Data Provider Utilities

combineDataProviders

Combine multiple data providers for different resources or namespaces.

import { combineDataProviders } from 'react-admin';

const combineDataProviders: (dataProviderMatcher: DataProviderMatcher) => DataProvider;

type DataProviderMatcher = (resource: string) => DataProvider;

Usage Example

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
  }
});

withLifecycleCallbacks

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;

Usage Example

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}` };
    }
  }
]);

HttpError

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);
}

Additional Data Provider Utilities

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;

Usage Example - fetchUtils

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);

Advanced Usage Examples

Custom Data Provider Implementation

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
};

Optimistic Updates Example

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

docs

admin-core.md

advanced.md

auth.md

data-management.md

detail-views.md

forms-inputs.md

i18n.md

index.md

layout-navigation.md

lists-data-display.md

ui-components.md

tile.json