Will King
Work In Progress

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 connect Reaction records to Content records

  • type: I am a fan of enum 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 a String type.

  • createdAt: Normally a record will have a created and updated timestamp. For this model we can leave off the updatedAt field since a Reaction either exists or it doesn't

  • user and userId: For this relationship we want to make our userId optional. This allows us to keep reactions from users that delete their account by setting the reaction to null

  • content and contentId: 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 {

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.


export async function like({
}: {
  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({
    type: "LIKE",

  return json<ActionResponse>({ success: true });



The UI Layer

Updates and More

Get updated when new articles, products, or components are released. Also, whatever else I feel like would be fun to send out.