# LLM docs
### Components
```tsx
// Server-rendered page (used to optimize speed and SEO)
export default async function ProjectPage({ params }: { params: Promise<{ projectId: string }>}) {
const { projectId } = await params
// Fetch with oRPC
const project = await client.projects.find({ projectId })
return ...
}
export default async function ProjectsListPage({ searchParams }: { searchParams: Promise<{ limit: number; cursor: number; }>}) {
const { limit, cursor } = await searchParams
const projects = await client.projects.list({ limit, cursor })
return ...
}
```
```tsx
// Client-side components only when needed
export function ProjectCard({ projectId }: { projectId: string }) {
const { data, isPending, error, refetch } = useQuery(
orpc.project.find.queryOptions({ input: { projectId }})
)
if (error) return (
<ErrorState title="Error fetching project" onRetry={refetch}>
Something went wrong when fetching project with id: {projectId}
</ErrorState>
)
if (isPending) return <ProjectCardSkeleton />
return <Card>...</Card>
}
```
```tsx
export function ListProjects() {
// Use nuqs for handling query strings for search, filter, etc
const [{ limit, cursor }, setPagination] = useQueryStates(
{
limit: parseAsInteger.withDefault(10),
cursor: parseAsInteger.withDefault(0)
}
)
// Use infinite query to easily paginate results
const { data, refetch, fetchNextPage, hasNextPage, error } = useSuspenseInfiniteQuery(
orpc.project.list.infiniteOptions({
input: cursor => ({ cursor, limit }),
getNextPageParam: lastPage => lastPage.length === limit ? lastPage.at(-1)?.id : null,
initialPageParam: cursor,
})
)
if (error) return (
<ErrorState title="Error fetching projects" onRetry={refetch}>
Something went wrong when listing projects
</ErrorState>
)
//
// Render Grid
return (
<Grid
columns={[1, 1, 2]}
data={data.pages.flat()}
renderItem={project => <ProjectCard key={project.id} projectId={project.id} />}
/>
)
// OR Render Table
return (
<Table>
<TableHead>
<TableRow>
<TableHeader>ID</TableHeader>
<TableHeader>Name</TableHeader>
<TableHeader>Description</TableHeader>
<TableHeader>Image</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{data.pages.flatMap((page, i) =>
page.map(project => (
<TableRow key={`${project.id}-${i}`}>
<TableCell>{project.id}</TableCell>
<TableCell>{project.name}</TableCell>
<TableCell>{project.description}</TableCell>
<TableCell>{project.imageUrl}</TableCell>
</TableRow>
)),
)}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={4}>
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage}
>
Load more
</button>
<button type="button" onClick={() => refetch()}>
Refresh
</button>
</TableCell>
</TableRow>
</TableFooter>
</Table>
)
}
```
#### Form components
```tsx
export function CreateProjectForm() {
// Always useForm directly in the form component to locate the form logic close to the rendered components
const form = useForm<z.infer<typeof projectInputSchema>>({
resolver: zodResolver(projectInputSchema),
defaultValues: {}
})
const { mutateAsync, isPending } = useMutation(
orpc.project.create.mutationOptions({
onSuccess() {
queryClient.invalidateQueries({ queryKey: orpc.project.key() })
},
onError(error) {
console.error(error)
toast.error(error.message)
},
}),
)
function onSubmit(values: z.infer<typeof projectInputSchema>) {
return mutateAsync(values)
}
const { isSubmitting } = form.formState
return (
<Form>
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Prefer the slighty more redundant way of using FormField directly rather than wrapping components for greater clarity (complex re-usable form elements with custom logic are fine to wrap) */}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Project Name</FormLabel>
<FormControl>
<Input placeholder="Project Name" {...field} />
</FormControl>
<FormDescription>This is the project display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
...
<Button type="submit" isLoading={isSubmitting} loadingText="Creating..." icon={PlusIcon}>
Create Project
</Button>
</form>
</Form>
)
}
```
### oRPC Router
```ts
import { listProjects, ... } from "./projects"
export const router = {
...
project: {
list: listProjects,
create: createProject,
find: findProject,
update: updateProject,
},
...
}
```
```ts
export const createProject = authed // or pub for public routes
.route({
method: 'POST',
path: '/projects',
summary: 'Create a project',
tags: ['Projects'],
})
.input(ProjectInputSchema) // Validates input
.output(ProjectSchema) // Validates returned data
.handler(async ({ input, context }) => {
return context.db.projects.create(input, context.user)
})
export const listProjects = pub
.route({
method: 'GET',
path: '/projects',
summary: 'List all projects',
tags: ['Projects'],
})
.input(
z.object({
limit: z.number().int().min(1).max(100).default(10),
cursor: z.number().int().min(0).default(0),
}),
)
.output(z.array(ProjectSchema))
.handler(async ({ input, context }) => {
return context.db.projects.select()
.orderBy(asc(projects.updatedAt))
.limit(input.limit)
.offset(input.cursor);
})
```