Creating a Reddit Clone Using React and GraphQL - 19
Tags: react, typescript, graphql, redditclone
Published: 2021-05-29 10:51:22

### This blog post originally posted on my blog site and you can find it [here](https://www.rasikag.com/posts/creating-a-reddit-clone-using-react-and-graphql-19).

From last post, we stopped with a bug that introduce to the system. If we check the console in API we can see there is an error.

```javascript
error: bind message supplies 2 parameters, but prepared statement "" requires 1

```

The thing is, in the `post` resolver, we are providing 2 parameters in the replacement array. But in the query, we only have `$1`. Because if the user just scroll the home page without login to the system, there will not be an `UserId` . So we need to change the parameters dynamically according to the query.

So first change the replacement array.

```javascript
const replacement: any[] = [realLimitPlusOne];
```

Then set the cursor index dynamically.

```javascript
if (req.session.userId) {
  replacement.push(req.session.userId);
}
let cursorIdx = 3;
if (cursor) {
  replacement.push(new Date(parseInt(cursor)));
  cursorIdx = replacement.length;
}
```

Then we can change the query like this.

```javascript
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"
// changeing the cursorIdx dynamically
${cursor ? ` WHERE  p."createdAt" < $${cursorIdx}` : ""}
ORDER BY p."createdAt" DESC
LIMIT $1

```

Now we are going to address on next issue in this application. Once you log in and refresh the page, you will not see any upvotes or downvotes that you did.

The reason for it, Once the browser sending a direct request to `graphql` `server`, it will attach the cookie. But when we are doing server-side rendering browser first send a request to `node.js `back-end then it will send to `graphql.` While this transition the cookie is dropping from that request.

So, let’s set the cookie in `createUrqlClient` .

```javascript
export const createUrqlClient = (ssrExchange: any, ctx: any) => {

// add context as parameter above
// then set the cookie value
let cookie = "";
if (isServer()) {
cookie = ctx?.req?.headers?.cookie;
}
// then set headers in fetchOptions
fetchOptions: {
credentials: "include" as const,
headers: cookie ? { cookie } : undefined,
},

```

Now we want to show the full post. First, we are going to add the back-end code for return a single post by its id with the creator’s details.

```javascript
@Query(() => Post, { nullable: true })
post(@Arg("id", () => Int) id: number): Promise<Post | undefined> {
// this should be match with post entity relation's property name
return Post.findOne(id, { relations: ["creator"] });
}

```

Then we are adding `graphql` query in front-end.

```javascript
query Post($id: Int!) {
post(id: $id) {
id
createdAt
updatedAt
title
text
points
voteStatus
creator {
id
username
}
}
}

```

Now use `yarn-gen` command to generate the TypeScript mapping methods. After that, we can change the `index.tsx` page to convert the title to a link.

```javascript
<NextLink href="/post/[id]" as={`/post/${p.id}`}>
  <Link>
    <Heading fontSize="xl">{p.title}</Heading>
  </Link>
</NextLink>
```

This will navigate to `post` page. Now we need to add the Post page. So we create a new folder called `pages` and add a file `[id].tsx` page. If you check that above navigation code to post’s details page we are setting the post’s id as url parameter. Because of that, we need to set the page’s name according to that.

We are using 2 utilities here. We can abstract functionality to get the post id from query as below.

```javascript
export const useGetIntId = () => {
  const router = useRouter();
  const intId =
    typeof router.query.id === "string" ? parseInt(router.query.id) : -1;
  return intId;
};
```

When the post page loading we need to call for the `graphql` API to get the full post. To do the we can use below utility.

```javascript
export const useGetPostFromUrl = () => {
  const intId = useGetIntId();
  return usePostQuery({
    pause: intId === -1,
    variables: {
      id: intId,
    },
  });
};
```

Now all set for the post page. Let’s add it.

```javascript
const Post = ({}) => {
  const [{ data, error, fetching }] = useGetPostFromUrl();

  if (fetching) {
    return (
      <Layout>
        <div>loading...</div>
      </Layout>
    );
  }
  if (error) {
    return <div>{error.message}</div>;
  }
  if (!data?.post) {
    return (
      <Layout>
        <Box>could not find post</Box>
      </Layout>
    );
  }
  return (
    <Layout>
      <Heading mb={4}>{data.post.title}</Heading>
      {data.post.text}
    </Layout>
  );
};
export default withUrqlClient(createUrqlClient, { ssr: true })(Post);
```

Also for the clean structure of the application, we can move the main application link and create post link to navigation bar.

```javascript
// in NavBar.tsx
body = (
<Flex align="center">
<NextLink href="/create-post">
<Button as={Link} mr={4}>
create post
</Button>
</NextLink>
<Box mr={2}>{data.me.username}</Box>
// ... remaining code goes here
// in return()
return (
<Flex zIndex={1} position="sticky" top={0} bg="tomato" p={4} align="center">
<Flex flex={1} m="auto" align="center" maxW={800}>
<NextLink href="/">
<Link>
<Heading>Reddit Clone</Heading>
</Link>
</NextLink>
<Box ml={"auto"}>{body}</Box>
</Flex>
</Flex>
);

```

Now we can add delete post functionality. First, we change the functionality that only the post’s owner can delete the post.

```javascript
@Mutation(() => Boolean)
async deletePost(
@Arg("id", () => Int) id: number,
@Ctx() { req }: RedditDbContext
): Promise<boolean> {
// const post = await Post.findOne(id);
// if (!post) {
//   return false
// }
// if (post.creatorId !== req.session.userId) {
//   throw new Error("not authorize")
// }
// await Upvote.delete({postId: id});
await Post.delete({ id, creatorId: req.session.userId });
return true;
}
}

```

Also, change the `Upvote.ts` file to set the cascade delete of upvotes.

```javascript
// in the Upvote.ts file, add onDelete property
@ManyToOne(() => Post, (post) => post.upvotes, {
onDelete: "CASCADE",
})
post: Post;

```

In the front-end app let’s add `graphql mutation` to delete post.

```javascript
mutation DeletePost($id: Int!) {
deletePost(id: $id)
}

```

Then we can add the button to delete a post and delete logic into `index.tsx` file.

```javascript
const [, deletePost] = useDeletePostMutation();
// below text snippet section
// the full code will be at Github link at the end.
// please check that for more clarity
<IconButton
  icon={<DeleteIcon />}
  aria-label="Delete Post"
  onClick={() => {
    deletePost({ id: p.id });
  }}
/>;
```

Once we delete a post we need to update the cache. So we are adding this mutation to `createUrqlClient` .

```javascript
deletePost: (_result, args, cache, info) => {
cache.invalidate({
__typename: "Post",
id: (args as DeletePostMutationVariables).id,
});
},
```

Now we are moving to create `edit post` functionality.

First, we are going to change the back-end code. Here is the `updatePost` method.

```javascript
@Mutation(() => Post)
async updatePost(
  @Arg("id", () => Int) id: number,
  @Arg("title", () => String, { nullable: true }) title: string,
  @Arg("text") text: string,
  @Ctx() { req }: RedditDbContext
): Promise<Post | null> {
  const result = await getConnection()
  .createQueryBuilder()
  .update(Post)
  .set({ title, text })
  .where('id = :id and "creatorId" = :creatorId', {
  id,
  creatorId: req.session.userId,
  })
  .returning("*")
  .execute();
  return result.raw[0];
}

```

Now, in the front-end, we need to have `UpdatePost` mutation.

```javascript
mutation UpdatePost($id: Int!, $title: String!, $text: String!) {
  updatePost(id: $id, title: $title, text: $text) {
    id
    title
    text
    textSnippet
  }
}

```

Then in the index.tsx file we add the link to navigate to edit post page.

```javascript
<NextLink href="/post/edit/[id]" as={`/post/edit/${p.id}`}>
  <IconButton mr={4} icon={<EditIcon />} aria-label="Edit Post" />
</NextLink>
```

Now we create a new folder inside the post folder. Then we create `[id].tsx` file. We can get the post id from url parameter and get the post to edit.

```javascript
const EditPost = ({}) => {
  const router = useRouter();
  const intId = useGetIntId();
  const [{ data, fetching }] = usePostQuery({
    pause: intId === -1,
    variables: {
      id: intId,
    },
  });
  const [, updatePost] = useUpdatePostMutation();
  if (fetching) {
    return (
      <Layout>
        <div>loading...</div>
      </Layout>
    );
  }

  if (!data?.post) {
    return (
      <Layout>
        <Box>could not find post</Box>
      </Layout>
    );
  }
  return (
    <Layout variant="small">
      <Formik
        initialValues={{ title: data.post.title, text: data.post.text }}
        onSubmit={async (values) => {
          await updatePost({ id: intId, ...values });
          router.push("/");
        }}
      >
        {({ isSubmitting }) => (
          <Form>
            <InputField name="title" placeholder="title" label="Title" />
            <Box mt={4}>
              <InputField
                textarea
                name="text"
                placeholder="text..."
                label="Body"
              />
            </Box>
            <Button
              isLoading={isSubmitting}
              mt={4}
              type="submit"
              colorScheme="teal"
            >
              Update post
            </Button>
          </Form>
        )}
      </Formik>
    </Layout>
  );
};

export default withUrqlClient(createUrqlClient)(EditPost);
```

Finally, for this post, we only want to add `edit` and `delete` buttons for post list, if post owned by log in user. Let’s add that validation to `index.tsx` file.

```javascript
// get the current log in user.
const [{ data: meData }] = useMeQuery();
{meData?.me?.id !== p.creator.id ? null : (
// wrap 2 buttons in here.
)
}

```

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](https://www.youtube.com/watch?v=I6ypD7qv3Z8). This is amazing tutorial and I highly recommend you to check that out.

Main image [credit](https://www.pexels.com/photo/lake-with-mountain-view-2440021/)

#### GitHub Links:
[Web App](https://github.com/rasikag/reddit-clone-web/tree/14_update_post_ui)
[Web Server](https://github.com/rasikag/reddit-clone/tree/14_update_post_ui)