Fetch Tasks by Project
Right now, the kanban board fetches every task in the database, regardless of which project is open. That was fine when we only had one flat list, but now every project has its own tasks. We need to scope the query.
The Wrong Way: Filter on the Client
One approach is to keep the current tasks.list query as-is and filter the results in the KanbanBoard component:
const allTasks = useQuery(api.tasks.list);
const tasks = allTasks?.filter((t) => t.projectId === projectId);
This works, but it is a bad idea. Here is why:
- Wasted bandwidth. You are sending every task for every project over the network, only to throw most of them away.
- Slower rendering. The component waits for the entire table to load before it can display anything.
- Does not scale. If there are 10,000 tasks across 100 projects, you are downloading all 10,000 to show the 100 that belong to the current project.
The backend already knows the current project. Let it do the filtering.
Update the Query: Filter on the Backend
Let’s update the tasks.list query in convex/tasks.ts to accept a projectId argument and filter on the server:
export const list = query({
args: {
projectId: v.id("projects"),
},
handler: async (ctx, args) => {
return await ctx.db
.query("tasks")
.filter((q) => q.eq(q.field("projectId"), args.projectId))
.collect();
},
});
Now the query accepts a projectId and uses .filter() to return only the tasks that belong to that project. The filtering happens on the Convex backend. Only the matching documents are sent to the client.
The .filter() method takes a callback that receives a query builder q. You use q.eq() to compare a field value against the argument. This is Convex’s filter syntax. It looks different from JavaScript’s .filter(), but it does the same thing.
If npx convex dev is running, the query change deploys automatically.
Update the KanbanBoard
Now update src/components/kanban-board.tsx to pass the projectId to the query and the optimistic update hook:
- const tasks = useQuery(api.tasks.list);
- const updateStatus = useUpdateTaskStatus();
+ const tasks = useQuery(api.tasks.list, { projectId });
+ const updateStatus = useUpdateTaskStatus(projectId);
Notice that we also updated the useUpdateTaskStatus hook to accept projectId. This is necessary because the optimistic update needs to know which cached query to update when a task’s status changes. We will update that hook next.
Update the TaskCard
The TaskCard component also calls useUpdateTaskStatus. Update src/components/task-card.tsx to pass the task’s projectId:
- const updateStatus = useUpdateTaskStatus();
+ const updateStatus = useUpdateTaskStatus(task.projectId);
Since each task already has a projectId field, we can pass it directly from the task document.
Update the Optimistic Update Hook
Update src/hooks/use-update-task-status.ts to work with the project-scoped query:
import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
import type { Id } from "../../convex/_generated/dataModel"; // 👀
function useUpdateTaskStatus(projectId: Id<"projects">) {
return useMutation(api.tasks.updateStatus).withOptimisticUpdate(
(localStore, args) => {
const tasks = localStore.getQuery(api.tasks.list, {
projectId, // 👀
});
if (!tasks) {
return;
}
localStore.setQuery(
api.tasks.list,
{ projectId }, // 👀
tasks.map((task) =>
task._id === args.id ? { ...task, status: args.status } : task,
),
);
},
);
}
export default useUpdateTaskStatus;
The hook now takes projectId as a parameter and uses it to read and write the correct cached query (api.tasks.list with the specific projectId).
Try it Out
Start the development server if it is not running:
pnpm run dev
This will run both the Convex backend and the React frontend. Open http://localhost:5173 in your browser, create at least two projects, and add a few tasks to each. You will see that only the tasks for the current project are fetched and displayed.
Checkpoint: Commit your progress.
git add .
git commit -m "planner-07: Fetch tasks by project using a database filter"
git push