Data Providers
Data providers are the core abstraction layer for data operations in React SuperAdmin. They provide a unified interface for CRUD operations, regardless of the underlying data source (database, API, mock data, etc.).
Overview
Data providers follow the React Admin pattern and implement a standardized interface that handles:
- CRUD Operations: Create, Read, Update, Delete
- List Operations: Pagination, filtering, sorting, search
- Relationship Handling: References and nested data
- Error Handling: Consistent error responses
- Type Safety: Full TypeScript support
Why Data Providers?
Data providers solve common challenges when building admin interfaces:
- Backend Agnostic: Switch between different data sources without changing your components
- Consistent API: Same interface regardless of whether you're using a database, REST API, or GraphQL
- Easy Testing: Use mock providers for isolated testing
- Runtime Switching: Change data sources without restarting your application
- Type Safety: Full TypeScript support across all providers
Quick Start
Basic Setup
- Wrap your app with DataProviderProvider:
import { DataProviderProvider } from '@react-superadmin/web';
function App() {
return (
<DataProviderProvider>
<YourApp />
</DataProviderProvider>
);
}
- Use the data provider in your components:
import { useDataProvider } from '@react-superadmin/web';
function UserList() {
const { dataProvider } = useDataProvider();
const fetchUsers = async () => {
const result = await dataProvider.getList('users', {
pagination: { page: 1, perPage: 10 },
sort: { field: 'name', order: 'ASC' },
});
return result.data;
};
}
Built-in Data Providers
1. Mock Data Provider
The mock data provider is perfect for development, testing, and prototyping. It uses localStorage for persistence and simulates real network behavior.
Features
- localStorage Persistence: Data survives page refreshes
- Network Simulation: Realistic delays for realistic testing
- Full CRUD: Complete create, read, update, delete operations
- Advanced Queries: Search, filtering, sorting, pagination
- Mock Data: Pre-populated with realistic sample data
- Type Safety: Full TypeScript support
Usage
import { mockDataProvider } from '@react-superadmin/core';
// The provider is automatically configured with mock data
const users = await mockDataProvider.getList('users', {
pagination: { page: 1, perPage: 10 },
sort: { field: 'name', order: 'ASC' },
filter: { role: 'admin' },
search: 'john',
});
Mock Data Structure
const mockData = {
users: [
{ id: '1', name: 'John Doe', email: 'john@example.com', role: 'admin' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com', role: 'user' },
],
posts: [
{ id: '1', title: 'First Post', content: 'Content...', authorId: '1' },
],
products: [
{ id: '1', name: 'Product A', price: 99.99, category: 'electronics' },
],
};
Configuration
import { dataProviderPresets } from '@react-superadmin/web';
const mockConfig = {
type: 'mock',
options: {
enableLocalStorage: true, // Persist data in localStorage
enableLogging: true, // Log operations to console
enableCaching: false, // No caching for mock provider
},
};
2. Prisma Data Provider
The Prisma data provider connects to real databases using Prisma ORM. It's production-ready and optimized for performance.
Features
- Database Integration: Connect to PostgreSQL, MySQL, SQLite, etc.
- Field Optimization: Automatic field selection for performance
- Complex Queries: Advanced filtering and search capabilities
- Transaction Support: Handle complex operations safely
- Connection Management: Automatic connection handling
- Error Handling: Comprehensive error management
Setup
-
Install Dependencies
pnpm add @prisma/client
pnpm add -D prisma -
Initialize Prisma
npx prisma init -
Define Schema (
prisma/schema.prisma)model User {
id String @id @default(cuid())
name String
email String @unique
role String @default("user")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
} -
Generate Client
npx prisma generate
Usage
import { prismaDataProvider } from '@react-superadmin/web';
const users = await prismaDataProvider.getList('users', {
pagination: { page: 1, perPage: 20 },
sort: { field: 'createdAt', order: 'DESC' },
filter: { role: 'admin' },
search: 'john',
});
Resource Field Configuration
Configure which fields to include for each resource:
const resourceFields = {
users: ['id', 'name', 'email', 'role', 'createdAt', 'updatedAt'],
posts: ['id', 'title', 'content', 'authorId', 'published', 'createdAt'],
products: ['id', 'name', 'price', 'category', 'inStock'],
};
Configuration
const prismaConfig = {
type: 'prisma',
options: {
enableLogging: process.env.NODE_ENV === 'development',
enableCaching: true,
databaseUrl: process.env.DATABASE_URL,
},
};
Creating Custom Data Providers
You can create custom data providers for any data source by implementing the DataProvider interface.
Basic Template
import {
DataProvider,
DataProviderParams,
ListResponse,
} from '@react-superadmin/core';
export class CustomDataProvider implements DataProvider {
async getList<T = any>(
resource: string,
params: DataProviderParams
): Promise<ListResponse<T>> {
// Implement list logic
throw new Error('Not implemented');
}
async getOne<T = any>(
resource: string,
params: GetOneParams
): Promise<{ data: T }> {
// Implement get one logic
throw new Error('Not implemented');
}
// ... implement all other methods
}
REST API Provider Example
export class RestApiDataProvider implements DataProvider {
private baseUrl: string;
private headers: Record<string, string>;
constructor(baseUrl: string, headers: Record<string, string> = {}) {
this.baseUrl = baseUrl;
this.headers = {
'Content-Type': 'application/json',
...headers,
};
}
async getList<T = any>(
resource: string,
params: DataProviderParams
): Promise<ListResponse<T>> {
const { pagination, sort, filter, search } = params;
// Build query parameters
const queryParams = new URLSearchParams();
if (pagination) {
queryParams.append('page', pagination.page.toString());
queryParams.append('perPage', pagination.perPage.toString());
}
if (sort) {
queryParams.append('sort', `${sort.field}:${sort.order}`);
}
if (search) {
queryParams.append('search', search);
}
// Add filters
Object.entries(filter || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
queryParams.append(`filter[${key}]`, value.toString());
}
});
const response = await fetch(
`${this.baseUrl}/${resource}?${queryParams.toString()}`,
{ headers: this.headers }
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
data: data.items,
total: data.total,
page: pagination?.page || 1,
perPage: pagination?.perPage || 10,
totalPages: Math.ceil(data.total / (pagination?.perPage || 10)),
};
}
async getOne<T = any>(
resource: string,
params: GetOneParams
): Promise<{ data: T }> {
const response = await fetch(`${this.baseUrl}/${resource}/${params.id}`, {
headers: this.headers,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return { data };
}
// ... implement other methods similarly
}
GraphQL Provider Example
export class GraphQLDataProvider implements DataProvider {
private client: any; // Apollo Client or similar
constructor(client: any) {
this.client = client;
}
async getList<T = any>(
resource: string,
params: DataProviderParams
): Promise<ListResponse<T>> {
const { pagination, sort, filter, search } = params;
const query = gql`
query Get${resource}($limit: Int, $offset: Int, $orderBy: String, $where: String) {
${resource}(limit: $limit, offset: $offset, orderBy: $orderBy, where: $where) {
id
# Add your fields here
}
${resource}Count(where: $where)
}
`;
const variables = {
limit: pagination?.perPage || 10,
offset: ((pagination?.page || 1) - 1) * (pagination?.perPage || 10),
orderBy: sort ? `${sort.field}_${sort.order}` : undefined,
where: this.buildWhereClause(filter, search),
};
const result = await this.client.query({ query, variables });
return {
data: result.data[resource],
total: result.data[`${resource}Count`],
page: pagination?.page || 1,
perPage: pagination?.perPage || 10,
totalPages: Math.ceil(
result.data[`${resource}Count`] / (pagination?.perPage || 10)
),
};
}
private buildWhereClause(
filter: Record<string, any>,
search?: string
): string {
// Implement GraphQL where clause building
return '';
}
// ... implement other methods
}
Data Provider Factory
The data provider factory allows you to switch between different providers at runtime.
Basic Usage
import { DataProviderProvider, useDataProvider } from '@react-superadmin/web';
import { dataProviderPresets } from '@react-superadmin/web';
function App() {
return (
<DataProviderProvider initialConfig={dataProviderPresets.development}>
<YourApp />
</DataProviderProvider>
);
}
function ProviderSwitcher() {
const { switchProvider, getConfig } = useDataProvider();
const switchToMock = () => {
switchProvider(dataProviderPresets.mock);
};
const switchToPrisma = () => {
switchProvider(dataProviderPresets.prisma);
};
return (
<div>
<p>Current: {getConfig().type}</p>
<button onClick={switchToMock}>Use Mock</button>
<button onClick={switchToPrisma}>Use Prisma</button>
</div>
);
}
Configuration Presets
export const dataProviderPresets = {
development: {
type: 'mock',
options: {
enableLocalStorage: true,
enableLogging: true,
enableCaching: false,
},
},
production: {
type: 'prisma',
options: {
enableLogging: false,
enableCaching: true,
},
},
testing: {
type: 'mock',
options: {
enableLocalStorage: false,
enableLogging: false,
enableCaching: false,
},
},
};
React Integration
Using the Hook
import { useDataProvider } from '@react-superadmin/web';
function UserList() {
const { dataProvider } = useDataProvider();
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const fetchUsers = async () => {
setLoading(true);
try {
const result = await dataProvider.getList('users', {
pagination: { page: 1, perPage: 20 },
sort: { field: 'name', order: 'ASC' },
});
setUsers(result.data);
} catch (error) {
console.error('Failed to fetch users:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchUsers();
}, []);
if (loading) return <div>Loading...</div>;
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
CRUD Operations
function UserForm() {
const { dataProvider } = useDataProvider();
const [formData, setFormData] = useState({});
const createUser = async () => {
try {
const result = await dataProvider.create('users', {
data: formData,
});
console.log('User created:', result.data);
} catch (error) {
console.error('Failed to create user:', error);
}
};
const updateUser = async (id: string) => {
try {
const result = await dataProvider.update('users', {
id,
data: formData,
});
console.log('User updated:', result.data);
} catch (error) {
console.error('Failed to update user:', error);
}
};
const deleteUser = async (id: string) => {
try {
await dataProvider.delete('users', { id });
console.log('User deleted');
} catch (error) {
console.error('Failed to delete user:', error);
}
};
}
Data Provider Methods
All data providers implement the same interface:
getList(resource, params)
Fetch a paginated list of records with filtering, sorting, and search.
const result = await dataProvider.getList('users', {
pagination: { page: 1, perPage: 20 },
sort: { field: 'createdAt', order: 'DESC' },
filter: { role: 'admin', active: true },
search: 'john doe',
});
getOne(resource, params)
Fetch a single record by ID.
const result = await dataProvider.getOne('users', { id: '123' });
getMany(resource, params)
Fetch multiple records by IDs.
const result = await dataProvider.getMany('users', { ids: ['1', '2', '3'] });
getManyReference(resource, params)
Fetch records related to another record.
const result = await dataProvider.getManyReference('posts', {
target: 'authorId',
id: '123',
pagination: { page: 1, perPage: 10 },
});
create(resource, params)
Create a new record.
const result = await dataProvider.create('users', {
data: { name: 'John Doe', email: 'john@example.com' },
});
update(resource, params)
Update an existing record.
const result = await dataProvider.update('users', {
id: '123',
data: { name: 'Jane Doe' },
});
updateMany(resource, params)
Update multiple records.
const result = await dataProvider.updateMany('users', {
ids: ['1', '2', '3'],
data: { role: 'admin' },
});
delete(resource, params)
Delete a record.
const result = await dataProvider.delete('users', { id: '123' });
deleteMany(resource, params)
Delete multiple records.
const result = await dataProvider.deleteMany('users', {
ids: ['1', '2', '3'],
});
Error Handling
All data providers include comprehensive error handling:
try {
const result = await dataProvider.getList('users', params);
// Handle success
} catch (error) {
if (error instanceof Error) {
console.error('Data provider error:', error.message);
// Show user-friendly error message
setError(error.message);
} else {
console.error('Unknown error:', error);
setError('An unexpected error occurred');
}
}
Performance Considerations
Mock Provider
- Pros: Fast, no network overhead, predictable performance
- Cons: Limited data size, no real persistence
- Use Case: Development, testing, prototyping
Prisma Provider
- Pros: Real data, scalable, optimized queries
- Cons: Network latency, database dependency
- Use Case: Production, large datasets
General Tips
- Pagination: Always use pagination for large datasets
- Field Selection: Only select needed fields
- Caching: Enable caching for production
- Indexing: Proper database indexing for Prisma provider
Testing
Mock Provider Testing
const testConfig = {
type: 'mock',
options: {
enableLocalStorage: false,
enableLogging: false,
},
};
Prisma Provider Testing
const testConfig = {
type: 'prisma',
options: {
enableLogging: false,
enableCaching: false,
},
};
Unit Testing
import { renderHook } from '@testing-library/react';
import { DataProviderProvider, useDataProvider } from '@react-superadmin/web';
test('should use mock provider by default', () => {
const wrapper = ({ children }) => (
<DataProviderProvider>{children}</DataProviderProvider>
);
const { result } = renderHook(() => useDataProvider(), { wrapper });
expect(result.current.getConfig().type).toBe('mock');
});
Migration Guide
From Old Services
If migrating from existing services like mockService.ts:
-
Replace imports
// Old
import { userService } from '../services/mockService';
// New
import { useDataProvider } from '@react-superadmin/web'; -
Update method calls
// Old
const users = await userService.list(params);
// New
const { dataProvider } = useDataProvider();
const result = await dataProvider.getList('users', params);
const users = result.data; -
Wrap your app
import { DataProviderProvider } from '@react-superadmin/web';
function App() {
return (
<DataProviderProvider>
<YourApp />
</DataProviderProvider>
);
}
Troubleshooting
Common Issues
-
Provider not found
- Ensure
DataProviderProviderwraps your app - Check that the provider is properly exported
- Ensure
-
Type errors
- Verify all required methods are implemented
- Check TypeScript configuration
-
Database connection (Prisma)
- Verify
DATABASE_URLenvironment variable - Check database server is running
- Run
npx prisma generateafter schema changes
- Verify
-
Mock data not persisting
- Check localStorage is enabled
- Verify browser supports localStorage
Debug Mode
Enable logging to debug provider operations:
const config = {
type: 'mock',
options: { enableLogging: true },
};
This will log all data provider method calls, parameters, and results to the console.
Best Practices
- Always handle errors - Wrap data provider calls in try-catch
- Use TypeScript - Leverage generic types for better type safety
- Implement pagination - Don't load all data at once
- Optimize queries - Only select needed fields
- Test thoroughly - Test with both mock and real providers
- Monitor performance - Use logging in development
- Plan for scaling - Design with future growth in mind
Examples
See the DataProviderDemo component in packages/web/src/components/examples/DataProviderDemo.tsx for a complete working example of data provider usage and switching.
Next Steps
- Components - Explore UI components
- Quick Start - Build your first admin panel
- Examples - See features in action
- Developer Guide: Hooks - Advanced hook usage