All Articles

Creating a Reddit Clone Using React and GraphQL - 14

inner peace

Let’s create a folder called middleware and add the isAuth.ts file and blow code to it.

import { RedditDbContext } from "../types";
import { MiddlewareFn } from "type-graphql";

export const isAuth: MiddlewareFn<RedditDbContext> = ({ context }, next) => {
  if (!context.req.session.userId) {
    throw new Error("not authenticated");
  }
  return next();
};

Then we can use this middleware in our post resolver. This @UseMiddleware is coming from type-graphql

@Mutation(() => Post)
@UseMiddleware(isAuth)
async createPost(
// ... remaining code

Now we are moving to the front-end app and going to add Post form. Let’s create create-post.tsx file in pages folder.

Let’s add this code to it.

const CreatePost: React.FC<{}> = ({}) => {
  return (
    <Wrapper variant="small">
      <Formik
        initialValues={{ title: "", text: "" }}
        onSubmit={async (values) => {}}
      >
        {({ isSubmitting }) => (
          <Form>
            <InputField name="title" placeholder="title" label="Title" />
            <Box mt={4}>
              <InputField name="text" placeholder="text..." label="Body" />
            </Box>
            <Button
              isLoading={isSubmitting}
              mt={4}
              type="submit"
              colorScheme="teal"
            >
              Create post
            </Button>
          </Form>
        )}
      </Formik>
    </Wrapper>
  );
};

export default CreatePost;

It is better to have a textarea for body field. We can change the InputField component. We can accept a prop called as textarea and define it as boolean.

... // InputFieldProps
textarea?: boolean

Then check it in the InputField component.

let InputOrTextarea = Input
if (textarea) {
  InputOrTextarea = Textarea
}
...
<InputOrTextarea
{...props}
{...field}
id={field.name}
placeholder={props.placeholder}
/>

Now we need to add the mutation. Create a file called createPost.graphql and add the below code. Then run yarn gen

mutation CreatePost($input: PostInput!) {
  createPost(input: $input){
    title
    id
    createdAt
    creatorId
    updatedAt
    text
    points
  }
}

Then we can use useCreatePostMutation and process the request to create a post. Also, in this create post page, we need to add the NavBar to this page. Let’s create a common component for it. Create a component called Layout.tsx and add this code block.

import { NavBar } from "./NavBar";
import { Wrapper, WrapperVariant } from "./Wrapper";

interface LayoutProps {
  // added WrapperVariant type
  variant?: WrapperVariant;
}

export const Layout: React.FC<LayoutProps> = ({ children, variant }) => {
  return (
    <>
      <NavBar />
      <Wrapper variant={variant}>{children}</Wrapper>
    </>
  );
};

Because we used Wrapper inside here. So we need to set the variant in the Layout. Without duplicating code we can create a type in there.

// Wrapper.tsx
export type WrapperVariant = "small" | "regular";
// change inside the Wrapper.tsx
interface WrapperProps {
  variant?: WrapperVariant;
}

Then replace it all over the places that we used variant. Then we can replace the Wrapper with Layout in the create-post.tsx page.

<Layout variant="small">...</Layout>

Now we can make this navbar sticky. Add the below code to make it sticky.

// NavBar.tsx
<Flex zIndex={1} position="sticky" top={0} bg="tomato" p={4}>
...

Now at this point, once user create a post without login to system, this will throw an unauthenticated error. We can simply manage it on onSubmit function.

// create-post.tsx
onSubmit={async (values) => {
  const { error } = await creatPost({ input: values });

  if (error?.message.includes("not authenticated")) {

   router.push("/login")
  } else {
   router.push("/");
  }
}}

The issue in this approach we need to check in every graphql query that user is authenticated or not. We can create a global error handler for this. Add below code to createUrqlClient.ts and add this exchanges .

import { pipe, tap } from "wonka";
const errorExchange: Exchange = ({ forward }) => (ops$) => {
  return pipe(
    forward(ops$),
    tap(({ error }) => {
      if (error?.message.includes("not authenticated")) {
        Router.replace("/login");
      }
    })
  );
};
// ...
// below in this file
exchanges: [
  // ...
  errorExchange,
  ssrExchange,
  fetchExchange,
];

Once a user tries to do something without login, this will navigate to login page.

We can make this better by checking user is logged in by checking me query. To do that we can create a hook. Inside the utils folder add a file called useIsAuth.ts and add the below code.

import { useRouter } from "next/router";
import { useEffect } from "react";
import { useMeQuery } from "../generated/graphql";

export const useIsAuth = () => {
  const [{ data, fetching }] = useMeQuery();
  const router = useRouter();
  useEffect(() => {
    if (!fetching && !data?.me) {
      router.replace("/login?next=" + router.pathname);
    }
  }, [fetching, data, router]);
};

In here we are adding router.pathname as query parameter. This will route back to the previous page that the user, where were before. Let’s user this in create-post.tsx page.

const CreatePost: React.FC<{}> = ({}) => {
const router = useRouter();
useIsAuth();
// ...

Then in the login page when there is a success login with next query parameter navigate to that page.

// ...
// inside the onSubmit method
else if (response.data?.login.user) {
  if (typeof router.query.next === "string") {
    router.push(router.query.next);
  } else {
    router.push("/");
  }
}

If there is no any error, navigate to the home page.

// inside the onSubmit method
const { error } = await creatPost({ input: values });
if (!error) {
  router.push("/");
}

Also, we can modify the token-passing mechanism in [token].tsx page , because the token also can access as a query value. So, let’s change that code also.

const ChangePassword: NextPage<{ token: string }> = () => {
...
// previously we took token as initial prop.
// const ChangePassword: NextPage<{ token: string }> = ({ token })
// ...
// get the token from the query parameter
const response = await changePassword({
newPassword: values.newPassword,
token:
typeof router.query.token === "string" ? router.query.token : "",
});
// then remove getInitialProps
// delete below code from the file
ChangePassword.getInitialProps = ({ query }) => {
return {
token: query.token as string,
};
};

Because we removed the getInitialProps , Next.JS is optimize this page to render it as a static page.


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