This documentation is for a legacy version of Remix Supabase. For the latest version, please visit the Remix Supabase Turbo documentation

Building the Task Detail page

Learn how to build the Task Detail page.

The Task Detail page is the page that shows the details of a specific task. It is a child page of the Tasks page.

As you may know, we can define a dynamic path in Remix using the convention $param denoted by the $1 prefix. For example, we can define a dynamic path for the Tasks page by creating a file called _app.tasks.$task.tsx. This will create a dynamic path for the Tasks page that will be available at /tasks/:task.

Building the Loader

First - we need to define the loader function for the Task Detail page. The loader function is responsible for fetching the data that will be used by the page.

In this case - we fetch the task data by using the getTask query. This query is defined in the ~/lib/tasks/queries module.

As you can see below, we're able to access the task param by using the args.params.task property. This is possible because we defined the dynamic path as _app.tasks.$task.tsx.

export const meta: MetaFunction = ({ data }) => { return [ { title: data.task.name, }, ]; }; export async function loader(args: LoaderArgs) { const client = getSupabaseServerClient(args.request); const taskId = args.params.task; const { data: task, error } = await getTask(client, Number(taskId)); if (!task || error) { return throwNotFoundException(); } return json({ task, }); }

Don't worry about copying the code above, we will show the full code for the Task Detail page later in this step.

Building the Action

From this page - we also allow users to update the task data. To do that, we define an action function that will be responsible for updating the task data.

export async function action(args: ActionArgs) { const client = getSupabaseServerClient(args.request); const body = await args.request.json(); switch (args.request.method) { case 'PUT': await updateTask(client, body); return json({ success: true }); } return throwNotFoundException(); }

Building the Task Detail Container

Before we build the page, we define a client component named TaskItemContainer that will be used by the page.

This component will be responsible for rendering the form that will be used to update the task - and also for handling the form submission using the useFetcher hook.

The useFetcher hook is a hook that is provided by Remix. When called, it will submit the action function defined above.

app/dashboard/[organization]/tasks/components/TaskItemContainer.tsx
function TaskItemContainer({ task, }: React.PropsWithChildren<{ task: Task; }>) { const fetcher = useFetcher(); const isSubmitting = fetcher.state === 'submitting'; const onUpdate: FormEventHandler<HTMLFormElement> = useCallback( (e) => { e.preventDefault(); const data = new FormData(e.currentTarget); const name = data.get('name') as string; const description = data.get('description') as string; fetcher.submit( { name, description, }, { method: 'PUT', encType: 'application/json', }, ); }, [fetcher], ); return ( <form onSubmit={onUpdate}> <div className={'flex flex-col space-y-4 max-w-xl'}> <Heading type={2}>{task.name}</Heading> <TextField.Label> Name <TextField.Input required name={'name'} defaultValue={task.name} /> </TextField.Label> <Label> Description <Textarea className={'h-32'} name={'description'} defaultValue={task.description} /> </Label> <div className={'flex space-x-2 justify-between'}> <Button href={'../tasks'} color={'transparent'}> <span className={'flex space-x-2 items-center'}> <ChevronLeftIcon className={'w-4'} /> <span>Back to Tasks</span> </span> </Button> <Button loading={isSubmitting}>Update Task</Button> </div> </div> </form> ); }

Building the Task Detail page

Now that we have the TaskItemContainer component, we can build the Task Detail page.

Below is the full code for the Task Detail page.

app/dashboard/[organization]/tasks/[task]/page.tsx
import type { MetaFunction } from '@remix-run/react'; import { useFetcher, useLoaderData } from '@remix-run/react'; import type { ActionArgs } from '@remix-run/node'; import { json } from '@remix-run/node'; import type { LoaderArgs } from '@remix-run/server-runtime'; import type { FormEventHandler } from 'react'; import React, { useCallback } from 'react'; import { ChevronLeftIcon, ArrowLeftIcon } from '@heroicons/react/24/outline'; import Heading from '~/core/ui/Heading'; import Button from '~/core/ui/Button'; import { throwNotFoundException } from '~/core/http-exceptions'; import getSupabaseServerClient from '~/core/supabase/server-client'; import AppHeader from '~/components/AppHeader'; import AppContainer from '~/components/AppContainer'; import { getTask } from '~/lib/tasks/queries'; import type Task from '~/lib/tasks/types/task'; import Label from '~/core/ui/Label'; import Textarea from '~/core/ui/Textarea'; import TextField from '~/core/ui/TextField'; import { updateTask } from '~/lib/tasks/mutations'; export const meta: MetaFunction = ({ data }) => { return [ { title: data.task.name, }, ]; }; export async function loader(args: LoaderArgs) { const client = getSupabaseServerClient(args.request); const taskId = args.params.task; const { data: task, error } = await getTask(client, Number(taskId)); if (!task || error) { return throwNotFoundException(); } return json({ task, }); } const TaskPage = () => { const data = useLoaderData<typeof loader>(); const task = data.task as Task; return ( <> <AppHeader> <TaskPageHeading /> </AppHeader> <AppContainer> <TaskItemContainer task={task} /> </AppContainer> </> ); }; function TaskPageHeading() { return ( <div className={'flex items-center space-x-6'}> <Heading type={4}> <span>Task</span> </Heading> <Button size={'small'} color={'transparent'} href={'/tasks'}> <ArrowLeftIcon className={'mr-2 h-4'} /> Back to Tasks </Button> </div> ); } export default TaskPage; function TaskItemContainer({ task, }: React.PropsWithChildren<{ task: Task; }>) { const fetcher = useFetcher(); const isSubmitting = fetcher.state === 'submitting'; const onUpdate: FormEventHandler<HTMLFormElement> = useCallback( (e) => { e.preventDefault(); const data = new FormData(e.currentTarget); const name = data.get('name') as string; const description = data.get('description') as string; fetcher.submit( { name, description, }, { method: 'PUT', encType: 'application/json', }, ); }, [fetcher], ); return ( <form onSubmit={onUpdate}> <div className={'flex flex-col space-y-4 max-w-xl'}> <Heading type={2}>{task.name}</Heading> <TextField.Label> Name <TextField.Input required name={'name'} defaultValue={task.name} /> </TextField.Label> <Label> Description <Textarea className={'h-32'} name={'description'} defaultValue={task.description} /> </Label> <div className={'flex space-x-2 justify-between'}> <Button href={'../tasks'} color={'transparent'}> <span className={'flex space-x-2 items-center'}> <ChevronLeftIcon className={'w-4'} /> <span>Back to Tasks</span> </span> </Button> <Button loading={isSubmitting}>Update Task</Button> </div> </div> </form> ); } export async function action(args: ActionArgs) { const client = getSupabaseServerClient(args.request); const body = await args.request.json(); switch (args.request.method) { case 'PUT': await updateTask(client, body); return json({ success: true }); } return throwNotFoundException(); }

Perfect, our Tasks App is now complete! 🎉

In the next steps, we take a look at some things you should now while keeping working on your app.


Subscribe to our Newsletter
Get the latest updates about React, Remix, Next.js, Firebase, Supabase and Tailwind CSS