All Articles

Creating a Reddit Clone Using React and GraphQL - 18

inner peace

From the last post, we added the back-end code. So now we need to add front-end code. Let’s add a separate component to UpvoteSection.tsx . In this component, we want to show up and down arrow buttons and current points that the post has. Also want to show that the status of our response here from different colours. If we upvoted it need to show green colour and downvoted need to show in the red colour.

Before adding that change, we are changing both vote and post methods to match with this. In thevote method we first check that we are upvoting or downvoting multiple times. If so nothing happens. If we are changing the vote we are updating the values accordingly. Replace the code in the vote from the below code form checking on userId .

const { userId } = req.session;

const upvote = await Upvote.findOne({ where: { postId, userId } });

if (upvote && upvote.value !== realValue) {
  await getConnection().transaction(async (tm) => {
    await tm.query(
      ` update upvote
set value = $1
where "postId" = $2 and "userId" = $3`,
      [realValue, postId, userId]
    );

    await tm.query(
      ` update post
set points = points + $1
where id = $2`,
      [2 * realValue, postId]
    );
  });
} else if (!upvote) {
  // has never voted before
  await getConnection().transaction(async (tm) => {
    await tm.query(
      ` insert into upvote ("userId", "postId", value)
values ($1, $2, $3)`,
      [userId, postId, realValue]
    );

    await tm.query(
      ` update post
set points = points + $1
where id = $2`,
      [realValue, postId]
    );
  });
}

Then add the new field to Post entity. This is not a column. This will only use for keep the voteStatus for each post for current logged user.

@Field(() => Int, { nullable: true })
voteStatus: number | null;

Also, need to change the posts query to get the data with voteStatus of the current user. To do that, replace the code with the below code.

// In the posts method add context as parameter
@Ctx() { req }: RedditDbContext
// add the values to replacement array by conditionaly
if (req.session.userId) {
  replacement.push(req.session.userId);
}
let cursorIdx = 3;
if (cursor) {
  replacement.push(new Date(parseInt(cursor)));

  cursorIdx = replacement.length;
}
// change the query to below query
SELECT p.*,
json_build_object(
'id', u.id,
'username', u.username,
'email', u.email
) creator,
${
req.session.userId
? '(SELECT value FROM upvote WHERE "userId" = $2 AND "postId" = p.id) "voteStatus"'
: 'null AS "voteStatus"'
}
FROM post p
INNER JOIN public.user u ON u.id = p."creatorId"
${cursor ? ` WHERE  p."createdAt" < $${cursorIdx}` : ""}
ORDER BY p."createdAt" DESC
LIMIT $1

We can get post related data to this component. Because in future there can need some other details from the post that we need to show in here or some other data to make desitions.

So we are moving post related data to a new fragment.

fragment PostSnippet on Post {
  id
  createdAt
  updatedAt
  title
  textSnippet
  points
  voteStatus
  creator {
    id
    username
  }
}

To match with this change, make a change to Posts query in the front-end.

query Posts($limit: Int!, $cursor: String) {
  posts(cursor: $cursor, limit: $limit) {
    hasMore
    posts{
      ...PostSnippet
    }
  }
}

Now is time to add the UpvoteSection . Here is the code for related that section.

interface UpvoteSectionProps {
  post: PostSnippetFragment;
}

export const UpvoteSection: React.FC<UpvoteSectionProps> = ({ post }) => {
  const [loadingState, setLoadingState] =
    (useState < "upvote-loading") |
    "downvote-loading" |
    ("not-loading" > "not-loading");
  const [, vote] = useVoteMutation();
  return (
    <Flex direction="column" justifyContent="center" alignItems="center" mr={4}>
      <IconButton
        onClick={async () => {
          setLoadingState("upvote-loading");
          await vote({
            postId: post.id,
            value: 1,
          });
          setLoadingState("not-loading");
        }}
        isLoading={loadingState === "upvote-loading"}
        aria-label="upvote post"
        colorScheme={post.voteStatus === 1 ? "green" : undefined}
        icon={<ChevronUpIcon />}
      />
      {post.points}
      <IconButton
        onClick={async () => {
          setLoadingState("downvote-loading");
          await vote({
            postId: post.id,
            value: -1,
          });
          setLoadingState("not-loading");
        }}
        isLoading={loadingState === "downvote-loading"}
        colorScheme={post.voteStatus === -1 ? "red" : undefined}
        aria-label="downvote post"
        icon={<ChevronDownIcon />}
      />
    </Flex>
  );
};

We can add this component to index.tsx file.

<Flex key={p.id} p={5} shadow="md" borderWidth="1px">
  <UpvoteSection post={p} />
  <Box>
    // add this before post title.
    <Heading fontSize="xl">{p.title}</Heading>
    <Text>posted by {p.creator.username}</Text>
    <Text mt={4}>{p.textSnippet}</Text>
  </Box>
</Flex>

Now the most important part. Once we are voted we are updating the current vote count. To do that we are using readFragment and writeFragment . What happened here, once we voted, we will request the new data from the graphql server by, passing postId . Once we receive the new values we are updating them. The main benefit is we are not requesting a full data set, but part of it. Here is the relevant vote mutation in createUrqlClient method. Also to do these we need to add grapgql-tag.

import gql from "graphql-tag";

vote: (_result, args, cache, info) => {
  const { postId, value } = args as VoteMutationVariables;
  const data = cache.readFragment(
    gql`
    fragment _ on Post {
      id
      points
      voteStatus
    }
    `,
    {
      id: postId,
    } as any
  );

  if (data) {

    if (data.voteStatus === value) {
      return;
    }
    const newPoints =
    (data.points as number) + (!data.voteStatus ? 1 : 2) * value;
    cache.writeFragment(
      gql`
      fragment __ on Post {
      points
      voteStatus
      }
      `,
      { id: postId, points: newPoints, voteStatus: value } as any
    );
  }
},

Thanks for reading this. If you have anything to ask regarding this please leave a comment here. Also, I wrote this according to my understanding. So if any point is wrong, don’t hesitate to correct me. I really appreciate you. That’s for today friends. See you soon. Thank you.

References:

This article series based on the Ben Award - Fullstack React GraphQL TypeScript Tutorial. This is amazing tutorial and I highly recommend you to check that out.

Main image credit