Data Fetching Patterns
The Employ platform follows specific patterns for fetching, managing, and displaying data. This guide shows you how to properly integrate with our API layer and build data-driven components.
Core Data Fetching Principles
- Always use composables for data fetching logic
- Leverage the tRPC-based API caller for type-safe API requests
- Separate data fetching from presentation
- Handle loading and error states consistently
The API Caller Pattern
All API calls should be made through the useApiCaller() composable, which provides a type-safe interface to our tRPC backend.
const { apiCaller } = useApiCaller();
// Example of fetching data
const result = await apiCaller.application.byJobId.query({
jobId: jobId.value,
limit: 100
});
Composable-Based Data Fetching
For reusable data-fetching logic, create dedicated composables:
// Example: modules/platform/shared/composables/useApplications.ts
export function useApplications(jobId: ComputedRef<string>) {
const { apiCaller } = useApiCaller();
const { toast } = useToast();
const { data, status, error, refresh } = useAsyncData(
`applications-${jobId.value}`,
async () => {
if (!jobId.value) return { items: [], total: 0 };
try {
const result = await apiCaller.application.byJobId.query({
jobId: jobId.value,
limit: 100,
});
return {
items: [...result.items],
nextCursor: result.nextCursor
};
} catch (error) {
throw error;
}
}
);
// Error handling
watchEffect(() => {
if (error.value) {
toast({
variant: "error",
title: "Error",
description: error.value.message || "Failed to load applications",
duration: 5000,
});
}
});
return {
applicationsData: data,
applicationsStatus: status,
applicationsError: error,
refreshApplications: refresh
};
}
Using Data in Components
When using data in components, unpack the composable results and pass them to child components:
<script setup lang="ts">
// In a page component
const jobId = ref('job-123');
const {
applicationsData,
applicationsStatus,
applicationsError
} = useApplications(computed(() => jobId.value));
</script>
<template>
<ApplicationList
:applications="applicationsData?.items"
:loading="applicationsStatus === 'pending'"
:error="applicationsError"
/>
</template>
Real-World Example: Applications Page
Here's how the Applications page implements these patterns:
<script setup lang="ts">
// Using the apiCaller for direct data fetching
const { apiCaller } = useApiCaller();
const { currentOrganization } = await useUser();
const { data: applications, status, error, refresh } = await useAsyncData(
`applications-${currentOrganization.value?.id}`,
async () => {
if (!currentOrganization.value?.id) return { items: [], total: 0 };
try {
// First fetch jobs for the organization
const jobsResult = await apiCaller.job.listByOrganization.query({
organizationId: currentOrganization.value.id,
});
if (!jobsResult.items || jobsResult.items.length === 0) {
return { items: [], total: 0 };
}
// Get all job IDs
const jobIds = jobsResult.items.map(job => job.id);
// Create a job lookup map
const jobMap = new Map();
jobsResult.items.forEach(job => {
jobMap.set(job.id, job);
});
// Fetch applications for all jobs
const result = await apiCaller.application.byJobId.query({
jobId: jobIds,
limit: 100
});
// Enrich applications with job data
const items = result.items.map(app => ({
...app,
job: {
id: app.jobId,
title: jobMap.get(app.jobId)?.title || "Unknown Job",
location: jobMap.get(app.jobId)?.location || null,
},
applicant: {
id: app.applicantId,
name: "Applicant",
email: "",
},
}));
return { items, total: items.length };
} catch (error) {
throw error;
}
},
);
// Update data when organization changes
watch(() => currentOrganization.value?.id, () => {
refresh();
});
</script>
Best Practices
- Cache Keys: Use unique cache keys for
useAsyncDatabased on relevant parametersuseAsyncData(`applications-${jobId.value}`, async () => { // ... }) - Reactive Dependencies: Update data when dependencies change
watch(() => currentOrganization.value?.id, () => { refresh(); }); - Error Handling: Always include error handling in your composables
watchEffect(() => { if (error.value) { toast({ variant: "error", title: "Error", description: error.value.message, }); } }); - Loading States: Always pass loading state to components
<ApplicationList :loading="status === 'pending'" /> - Data Transformation: Transform API data to match the component's expected format
const items = result.items.map(app => ({ ...app, job: { id: app.jobId, title: jobMap.get(app.jobId)?.title || "Unknown Job", }, }));
By following these patterns, you'll create maintainable, reusable data-fetching logic that keeps your components clean and focused on presentation.