🖥️ Editing Games
In this guide, we’ll add functionality that allows users to edit existing games in the database. We’ll create an edit page similar to our add game form, but pre-populated with the game’s current data, and then link to it from the game card.
Understanding the Challenge
Section titled “Understanding the Challenge”To implement edit functionality, we need to:
- Create a route that displays a form for editing a game
- Pre-populate the form with the game’s current data
- Handle form submission to update the game in the database
- Add a way for users to navigate to the edit page
Step 1: Create the Edit Game Page
Section titled “Step 1: Create the Edit Game Page”First, we’ll create a new route file for editing games. This will be similar to our add game form, but with some key differences to handle loading and updating existing data.
Add a new file called edit-game.$gameId.tsx
inside app/routes/
, and add the following code:
import { useState } from "react";import { Form, Link, useLoaderData } from "@remix-run/react";import { json, redirect } from "@remix-run/node";import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";import { PrismaClient } from "@prisma/client";import ImageUploader from "~/components/ImageUploader";
export async function loader({ params }: LoaderFunctionArgs) { const gameId = params.gameId;
const prisma = new PrismaClient();
const game = await prisma.game.findUnique({ where: { id: gameId }, });
if (!game) { throw new Response("Game not found", { status: 404 }); }
const categories = await prisma.category.findMany({ select: { id: true, title: true }, orderBy: { title: "asc" }, });
prisma.$disconnect();
return json({ game, categories });}
export async function action({ request, params }: ActionFunctionArgs) { const gameId = params.gameId; const formData = await request.formData(); const title = formData.get("title") as string; const description = formData.get("description") as string; const price = parseFloat(formData.get("price") as string); const rating = parseFloat(formData.get("rating") as string); const releaseDate = new Date(formData.get("releaseDate") as string); const imageUrl = formData.get("imageUrl") as string; const categoryId = formData.get("categoryId") as string;
const prisma = new PrismaClient();
await prisma.game.update({ where: { id: gameId }, data: { title, description, price, rating, releaseDate, imageUrl, categoryId, }, });
prisma.$disconnect();
return redirect("/");}
export default function EditGame() { const { game, categories } = useLoaderData<typeof loader>(); const [imageUrl, setImageUrl] = useState(game.imageUrl || "");
const handleImageUploaded = (url: string) => { setImageUrl(url); };
// Format the date to YYYY-MM-DD for the date input const formattedDate = new Date(game.releaseDate).toISOString().split("T")[0];
return ( <div className="container mx-auto py-20 px-4"> <h1 className="font-bold text-5xl text-center mb-10"> Edit <span className="text-cyan-400">Game</span> </h1>
<div className="max-w-2xl mx-auto bg-gray-950 p-8 rounded-xl"> <Form method="post" className="space-y-6"> <input type="hidden" name="imageUrl" value={imageUrl} />
{/* Form fields with defaultValue set to current game data */} <div> <label htmlFor="title" className="block text-sm font-medium mb-2 text-slate-400" > Title </label> <input type="text" id="title" name="title" defaultValue={game.title} required className="w-full p-3 bg-gray-800 rounded-md focus:outline-none focus:ring-2 focus:ring-cyan-500" /> </div>
<div> <label htmlFor="description" className="block text-sm font-medium mb-2 text-slate-400" > Description </label> <textarea id="description" name="description" defaultValue={game.description} required rows={4} className="w-full p-3 bg-gray-800 rounded-md focus:outline-none focus:ring-2 focus:ring-cyan-500" ></textarea> </div>
<div className="mb-8"> <ImageUploader onImageUploaded={handleImageUploaded} /> {imageUrl && ( <div className="mt-2"> <p className="text-sm text-slate-400">Current image:</p> <img src={imageUrl} alt={game.title} className="mt-1 h-20 object-cover rounded-md" /> </div> )} </div>
<div className="grid grid-cols-2 gap-4"> <div> <label htmlFor="price" className="block text-sm font-medium mb-2 text-slate-400" > Price </label> <input type="number" id="price" name="price" defaultValue={game.price} step="0.01" min="0" required className="w-full p-3 bg-gray-800 rounded-md focus:outline-none focus:ring-2 focus:ring-cyan-500" /> </div>
<div> <label htmlFor="rating" className="block text-sm font-medium mb-2 text-slate-400" > Rating </label> <input type="number" id="rating" name="rating" defaultValue={game.rating} step="0.1" min="0" max="5" required className="w-full p-3 bg-gray-800 rounded-md focus:outline-none focus:ring-2 focus:ring-cyan-500" /> </div> </div>
<div> <label htmlFor="releaseDate" className="block text-sm font-medium mb-2 text-slate-400" > Release Date </label> <input type="date" id="releaseDate" name="releaseDate" defaultValue={formattedDate} required className="w-full p-3 bg-gray-800 rounded-md focus:outline-none focus:ring-2 focus:ring-cyan-500" /> </div>
<div> <label htmlFor="categoryId" className="block text-sm font-medium mb-2 text-slate-400" > Category </label> <select id="categoryId" name="categoryId" defaultValue={game.categoryId || ""} required className="w-full p-3 bg-gray-800 rounded-md focus:outline-none focus:ring-2 focus:ring-cyan-500" > <option value="">Select a category</option> {categories.map((category) => ( <option key={category.id} value={category.id}> {category.title} </option> ))} </select> </div>
<div className="flex justify-end gap-16"> <Link to="/" className="text-red-300 border-2 border-red-300 py-3 px-6 rounded-md hover:bg-red-50/10 transition duration-200" > Cancel </Link> <button type="submit" className="bg-cyan-600 text-white py-3 px-6 rounded-md hover:bg-cyan-500 transition duration-200" > Update Game </button> </div> </Form> </div> </div> );}
Step 2: Add a Link to the Edit Page from the Game Card
Section titled “Step 2: Add a Link to the Edit Page from the Game Card”Now that we have an edit page, we need a way for users to navigate to it. We’ll update the GameCard
component to make the game image clickable, linking to the edit page.
import { Form } from "@remix-run/react";import { Form, Link } from "@remix-run/react";
interface GameCardProps { id: string; title: string; releaseDate: string; genre: string; imageUrl: string;}export default function GameCard({ id, title, releaseDate, genre, imageUrl,}: GameCardProps) { const formattedDate = releaseDate.substring(0, 10);
return ( <div className="flex flex-col gap-4"> <div className="relative h-72 overflow-hidden"> <Link to={`/edit-game/${id}`} className="relative h-72 overflow-hidden"> <img src={imageUrl} alt={`${title} cover`} className="absolute inset-0 w-full h-full object-cover rounded-xl" /> </div> </Link> <div className="flex justify-between"> <div className="flex flex-col justify-between w-2/3 pr-4"> <h3 className="font-bold text-2xl text-slate-300">{title}</h3>
Understanding Key Concepts
Section titled “Understanding Key Concepts”Dynamic Routes with Parameters
Section titled “Dynamic Routes with Parameters”In our edit page, we’re using a dynamic route parameter ($gameId
) to identify which game to edit. This is a powerful feature of Remix’s file-based routing system:
- The filename
edit-game.$gameId.tsx
creates a route that matches URLs like/edit-game/123
- The
$gameId
part becomes available asparams.gameId
in both the loader and action functions - This allows us to fetch and update the specific game identified in the URL
Form Pre-population
Section titled “Form Pre-population”Pre-populating form fields with existing data is a key aspect of edit functionality. In React, there are two approaches:
- Controlled components: The form field values are controlled by React state
- Uncontrolled components with defaultValue: The initial values are set, but the DOM handles subsequent updates
In our implementation, we’re using the second approach with defaultValue
. This is simpler and works well for most edit forms. The defaultValue
prop sets the initial value of the input, but doesn’t control it afterward.
Prisma Update Operations
Section titled “Prisma Update Operations”Updating data with Prisma follows a similar pattern to creating data, but with a few key differences:
- We use the
update
method instead ofcreate
- We specify which record to update using the
where
clause - We provide the new data in the
data
object
This pattern is consistent across Prisma operations, making it easy to learn and use.
Client-Side Navigation with Link
Section titled “Client-Side Navigation with Link”The Link
component from Remix enables client-side navigation, which has several benefits:
- Faster page transitions (no full page reload)
- Preserved JavaScript state
- Automatic prefetching of linked pages when hovering
- Fallback to regular navigation if JavaScript fails
By wrapping the game image in a Link
, we’re creating an intuitive way for users to access the edit functionality.
What We’ve Learned
Section titled “What We’ve Learned”In this tutorial, we’ve:
- Created a route for editing games with a pre-populated form
- Implemented the server-side logic to update games in the database
- Added a link from the game card to the edit page
- Learned about dynamic routes, form pre-population, and Prisma update operations
- Explored client-side navigation with Remix’s
Link
component
These changes complete the “U” in CRUD functionality for our application, allowing users to update existing games in the database.
Excellent work! You’ve successfully implemented edit functionality in your application, which is a crucial part of any data management system. With this addition, your GameLog application now supports creating, reading, updating, and deleting games - the full CRUD spectrum. This makes your application much more useful and complete.