How to add likes and upvoting to your Remix app
When it comes to these how-to articles I am going to approach the implementation by splitting it into 3 layers:
Data layer
Server layer
UI layer
Before we get started let's define exactly what we are building
On my website likes and upvotes are methods for reacting to a piece of content, and there are requirements that I defined while designing the features that will drive the development process:
One reaction type per user on a piece of content
It either exists or it doesn't. There are no negative reactions.
In Progress content receives votes to indicate interest from members
Competed content receives likes.
The Data Layer
In the data layer we are going to add our new Reaction
model to our schema.prisma
file. Let's break down each field in the model so that we can understand how it is working:
id
: This is needed to be able to index and connectReaction
records toContent
recordstype
: I am a fan ofenum
types in a situation like this where the possible options are very constrained. If you are not a fan of enums you can just use aString
type.createdAt
: Normally a record will have a created and updated timestamp. For this model we can leave off theupdatedAt
field since aReaction
either exists or it doesn'tuser
anduserId
: For this relationship we want to make ouruserId
optional. This allows us to keep reactions from users that delete their account by setting the reaction tonull
content
andcontentId
: For this relationship we want a more strict requirement. If the content is deleted that the reaction was made onto we have no reason to have the reactions any more.
I have also included an abridged version of the Content and User models so that you can see the paired relationship fields.
enum ReactionType {
UPVOTE
LIKE
}
model Reaction {
id String @id @default(cuid())
type ReactionType
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
userId String?
content Content @relation(fields: [contentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
contentId String
}
model Content {
...
reactions Reaction[]
}
model User {
...
reactions Reaction[]
}
The Server Layer
In the server layer we are going to be building out the functionality to handle creating, deleting, and reading our reactions.
Create
export async function like({
request,
formData,
}: {
request: Request;
formData: FormData;
}) {
const userId = await requireUserId(request);
const contentId = formData.get("contentId");
if (!contentId || typeof contentId !== "string") {
throw notFound({
message: "Failed to like content. Missing content ID.",
});
}
await createUserReaction({
userId,
contentId,
type: "LIKE",
});
return json<ActionResponse>({ success: true });
}