[ PART 15 ][Frontend] Creating a Twitter clone with GraphQL, Typescript, and React ( Authentication )
Tags: showdev, graphql, react, career
Published: 2021-01-17 17:54:07

Hi everyone ;).

As a reminder, I'm doing this [Tweeter challenge](https://devchallenges.io/challenges/rleoQc34THclWx1cFFKH)

[Github repository](https://github.com/ips-coding-challenge/challenge_twitter)

[Db diagram](https://dbdiagram.io/d/5f75c0243a78976d7b75f7be)



Before starting to code, I'd like to discuss about the way I will implement the authentication for **now**. I really wanted to focus on GraphQL for this project so I didn't want to take too much time on the authentication. But I changed my mind a little bit. For now, I will save the token in the localStorage and then pass it as an **authorization** header. However, I think I will write an article about this specific issue and discuss about my use case to have feedback from more experienced people. I read a lot about spa's authentication and it's pretty complex. That's why I'd like to discuss that in its own article ;). Be aware that localStorage is vulnerable to XSS attacks.


## Register page

*src/pages/Register.tsx*

```tsx
import { useMutation } from '@apollo/client'
import { yupResolver } from '@hookform/resolvers/yup'
import React, { useState } from 'react'
import { useForm } from 'react-hook-form'
import { MdEmail, MdLock, MdPeople } from 'react-icons/md'
import { useHistory } from 'react-router-dom'
import { useSetRecoilState } from 'recoil'
import Alert from '../components/Alert'
import Button from '../components/Button'
import Input from '../components/Input'
import Layout from '../components/Layout'
import { REGISTER } from '../graphql/auth/mutations'
import { userState } from '../state/userState'
import { handleErrors } from '../utils/utils'
import { registerSchema } from '../validations/auth/schema'

const Register = () => {
  const setUser = useSetRecoilState(userState)

  const [registerMutation, { loading }] = useMutation(REGISTER)
  const { register, handleSubmit, errors } = useForm({
    resolver: yupResolver(registerSchema),
  })
  const [serverErrors, setServerErrors] = useState<any>([])
  const history = useHistory()

  const registerUser = async (formData: any) => {
    setServerErrors([])
    try {
      const res = await registerMutation({
        variables: {
          input: formData,
        },
      })

      const { token, user } = res.data.register

      localStorage.setItem('token', token)
      setUser(user)
      history.push('/')
    } catch (e) {
      setServerErrors(handleErrors(e))
    }
  }

  return (
    <Layout>
      <h1 className="text-3xl mb-4 font-bold">Register</h1>
      <form className="w-full" onSubmit={handleSubmit(registerUser)}>
        {serverErrors.length > 0 && (
          <div className="mb-4">
            {serverErrors.map((e: any) => (
              <Alert variant="danger" message={e.message} />
            ))}
          </div>
        )}
        <Input
          label="Enter your username"
          name="username"
          icon={<MdPeople />}
          ref={register}
          error={errors.username?.message}
        />

        <Input
          label="Enter your Display Name"
          name="display_name"
          icon={<MdPeople />}
          ref={register}
          error={errors.display_name?.message}
        />

        <Input
          label="Enter your email"
          name="email"
          type="email"
          icon={<MdEmail />}
          ref={register}
          error={errors.email?.message}
        />

        <Input
          label="Enter your password"
          name="password"
          type="password"
          icon={<MdLock />}
          ref={register}
          error={errors.password?.message}
        />

        <Button
          disabled={loading}
          type="submit"
          text="Register"
          variant="primary"
        />
      </form>
    </Layout>
  )
}

export default Register

```

There are a lot of things going on here. Let's start with my custom **Input** and **Button** components

*src/components/Input.tsx*

```tsx
import { forwardRef, InputHTMLAttributes } from 'react'

type InputProps = {
  icon?: JSX.Element
  error?: string
  label?: string
} & InputHTMLAttributes<HTMLInputElement>

const Input = forwardRef(
  ({ icon, error, label, ...rest }: InputProps, ref: any) => {
    return (
      <div className="mb-4">
        {label && (
          <label className="text-sm" htmlFor={rest.name}>
            {label}
          </label>
        )}
        <div className="bg-gray1 flex items-center border px-2 py-1 border-gray2 rounded-lg ">
          {icon}

          <input
            id={rest.name}
            style={{ minWidth: 0 }}
            className="bg-transparent placeholder-gray4 ml-2 w-full h-full p-2 rounded-lg"
            {...rest}
            ref={ref}
          />
        </div>
        {error && <p className="text-red-500 text-sm">{error}</p>}
      </div>
    )
  }
)

export default Input

```

I need to create a forwardRef since I will use the **react-hook-form** to control the input. So I need to pass the ref to register the input.

*src/components/Button.tsx*

```tsx
import { ButtonHTMLAttributes } from 'react'

type ButtonProps = {
  text: string
  variant: string
  icon?: JSX.Element
  alignment?: 'left' | 'right'
} & ButtonHTMLAttributes<HTMLButtonElement>

const classes: any = {
  primary: 'bg-primary text-white hover:bg-primary_hover',
}

const Button = ({
  text,
  variant,
  icon,
  alignment = 'left',
  ...rest
}: ButtonProps) => {
  return (
    <button
      className={`${classes[variant]} flex items-center justify-center px-4 py-2 rounded transition-colors duration-300`}
      {...rest}
    >
      {icon && alignment === 'left' && <div className="mr-2">{icon}</div>}
      {text}
      {icon && alignment === 'right' && <div className="ml-2">{icon}</div>}
    </button>
  )
}

export default Button

```

I also extended the **tailwindcss.config.js** to define some colors and other variables.

For the validation's rules, I will use **yup** with the **yup resolver** from **react-hook-form**. Here is the schema that I will use. Note that I also modified the pattern in the backend for the Display Name.

*src/validations/auth/schema.ts*

```typescript
import * as yup from 'yup'

export const registerSchema = yup.object().shape({
  username: yup
    .string()
    .trim()
    .matches(
      /^[\w]{2,30}$/,
      'The username should only contains alphanumeric characters, underscores, and should have a length between 2 to 30'
    )
    .required(),
  email: yup.string().trim().email().required(),
  display_name: yup
    .string()
    .trim()
    .matches(
      /^[\w\s]{2,30}$/,
      'The display name should only contains alphanumeric characters, spaces, underscores and should have a length between 2 to 30'
    )
    .required(),
  password: yup.string().min(6).required(),
})
```

One thing that I don't like is how the validation errors are formatted from the **class-validator** library. I created two utility functions for now.

*src/utils/utils.ts*

```typescript
import { ApolloError } from '@apollo/client'

export const formatValidationErrors = (errors: any) => {
  let newErrors: any = []
  if (errors[0].message !== 'Argument Validation Error') {
    return errors[0]
  }
  const validationErrors = errors[0].extensions.exception?.validationErrors

  validationErrors.forEach((error: any) => {
    const field = error.property
    const constraints = error.constraints
    newErrors.push({
      field,
      message: Object.values(constraints)[0],
    })
  })

  return newErrors
}

export const handleErrors = (e: any) => {
  let errors = []
  if (e instanceof ApolloError) {
    if (
      e.graphQLErrors &&
      e.graphQLErrors[0].message === 'Argument Validation Error'
    ) {
      errors.push(formatValidationErrors(e.graphQLErrors))
    } else {
      errors.push(e)
    }
  } else {
    errors.push(e)
  }
  return errors
}

```

*src/graphql/auth/mutations.ts*

```typescript
export const REGISTER = gql`
  mutation($input: RegisterPayload!) {
    register(input: $input) {
      token
      user {
        id
        username
        display_name
        email
        created_at
        updated_at
      }
    }
  }
`
```



Otherwise, nothing too particular for the registration. I make use of the useMutation from **@apollo/client** and handle the result. If the request is successful, I save the token in localStorage and also set the user in my global store. For that, I use the **recoil** library. It's really easy to setup.

**index.tsx**

```tsx
import { ApolloProvider } from '@apollo/client'
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import client from './client'
import { RecoilRoot } from 'recoil'
import './styles/index.css'

ReactDOM.render(
  <RecoilRoot>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </RecoilRoot>,
  document.getElementById('root')
)

```

I wrap everything in the **<RecoilRoot></RecoilRoot>** component. I will then create a user *atom* to save my user.

*src/state/userState.ts*

```typescript
export const userState = atom({
  key: 'userState',
  default: null,
})

```

When I need to store the user, I use the *useSetRecoilState(userState)* hook from recoil. You also have the *useRecoilValue* if you just want to read the value. And finally, if you need to read or write the value, you have to use the *useRecoilState*.

Once the registration is complete, I redirect the user to my Home page.

Here is what the Register Page looks like:

![Register Page](https://dev-to-uploads.s3.amazonaws.com/i/2b1hmnrq9dztzn05sia9.png)



## Login Page

*src/pages/Login*

```tsx
import { ApolloError, useMutation } from '@apollo/client'
import { yupResolver } from '@hookform/resolvers/yup'
import React, { useState } from 'react'
import { useForm } from 'react-hook-form'
import { MdPeople, MdEmail, MdLock } from 'react-icons/md'
import { useHistory } from 'react-router-dom'
import { useSetRecoilState } from 'recoil'
import Alert from '../components/Alert'
import Button from '../components/Button'
import Input from '../components/Input'
import Layout from '../components/Layout'
import { LOGIN } from '../graphql/auth/mutations'
import { userState } from '../state/userState'
import { handleErrors } from '../utils/utils'
import { loginSchema } from '../validations/auth/schema'

const Login = () => {
  const setUser = useSetRecoilState(userState)

  const [loginMutation, { loading }] = useMutation(LOGIN)
  const { register, handleSubmit, errors } = useForm({
    resolver: yupResolver(loginSchema),
  })
  const [serverErrors, setServerErrors] = useState<any>([])
  const history = useHistory()

  const loginUser = async (formData: any) => {
    console.log('formData', formData)
    setServerErrors([])
    try {
      const res = await loginMutation({
        variables: {
          input: formData,
        },
      })

      const { token, user } = res.data.login

      localStorage.setItem('token', token)
      setUser(user)
      history.push('/')
    } catch (e) {
      if (e instanceof ApolloError) {
        setServerErrors(handleErrors(e))
      }
    }
  }

  return (
    <Layout>
      <h1 className="text-3xl mb-4 font-bold">Login</h1>
      <form className="w-full" onSubmit={handleSubmit(loginUser)}>
        {serverErrors.length > 0 && (
          <div className="mb-4">
            {serverErrors.map((e: any) => (
              <Alert variant="danger" message={e.message} />
            ))}
          </div>
        )}

        <Input
          label="Enter your email"
          name="email"
          type="email"
          icon={<MdEmail />}
          ref={register}
          error={errors.email?.message}
        />

        <Input
          label="Enter your password"
          name="password"
          type="password"
          icon={<MdLock />}
          ref={register}
          error={errors.password?.message}
        />

        <Button
          disabled={loading}
          type="submit"
          text="Login"
          variant="primary"
        />
      </form>
    </Layout>
  )
}

export default Login

```

It's really similar to the Register Page...

*src/graphql/mutations*

```typescript
export const LOGIN = gql`
  mutation($input: LoginPayload!) {
    login(input: $input) {
      token
      user {
        id
        username
        display_name
        email
        created_at
        updated_at
      }
    }
  }
`
```



![Login Page](https://dev-to-uploads.s3.amazonaws.com/i/wplegap4arfvgsxd56ls.png)

## Private / Public Page

In the application, the login/register pages should only be accessible if a user is not connected. Conversely, the home page should only be accessible if I have a connected user. To do that, I will create two components.

*src/components/PublicRoute.tsx*

```tsx
import { Redirect, Route, RouteProps } from 'react-router-dom'
import { useRecoilValue } from 'recoil'
import { userState } from '../state/userState'

type PublicRouteProps = {
  children: React.ReactNode
} & RouteProps

const PublicRoute = ({ children, ...rest }: PublicRouteProps) => {
  const user = useRecoilValue(userState)

  return (
    <Route
      {...rest}
      render={() => (!user ? children : <Redirect to={{ pathname: '/' }} />)}
    />
  )
}

export default PublicRoute

```

*src/components/PrivateRoute.tsx*

```tsx
import { Redirect, Route, RouteProps } from 'react-router-dom'
import { useRecoilValue } from 'recoil'
import { userState } from '../state/userState'

type PrivateRouteProps = {
  children: React.ReactNode
} & RouteProps

const PrivateRoute = ({ children, ...rest }: PrivateRouteProps) => {
  const user = useRecoilValue(userState)

  return (
    <Route
      {...rest}
      render={() =>
        user ? children : <Redirect to={{ pathname: '/login' }} />
      }
    />
  )
}

export default PrivateRoute

```

I then need to wrap my Route using these two components:

*App.tsx*

```tsx
import React from 'react'
import { BrowserRouter as Router, Switch } from 'react-router-dom'
import Navbar from './components/Navbar'
import PrivateRoute from './components/PrivateRoute'
import PublicRoute from './components/PublicRoute'
import { useInitAuth } from './hooks/useInitAuth'
import Home from './pages/Home'
import Login from './pages/Login'
import Register from './pages/Register'

function App() {
  const { init } = useInitAuth()

  if (init) return <div>Loading...</div>

  return (
    <Router>
      <Switch>
        <PublicRoute exact path="/login">
          <Login />
        </PublicRoute>
        <PublicRoute exact path="/register">
          <Register />
        </PublicRoute>
        <PrivateRoute exact path="/">
          <Home />
        </PrivateRoute>
      </Switch>
    </Router>
  )
}

export default App

```

You can also see a custom Hook here. The **useInitAuth** will initialize the app making a request if I have a token in the localStorage to fetch the user and then redirecting the user to the correct path.

*src/hooks/useInitAuth.ts* ( I added comments to explain what's going on )

```tsx
import { useLazyQuery } from '@apollo/client'
import { useCallback, useEffect, useState } from 'react'
import { useRecoilState } from 'recoil'
import { ME } from '../graphql/auth/queries'
import { userState } from '../state/userState'

export const useInitAuth = () => {
  const [user, setUser] = useRecoilState(userState)
  const [init, setInit] = useState(true)
  const [me, { data, loading, error }] = useLazyQuery(ME)

  const fetchUser = useCallback(async () => {
    const token = localStorage.getItem('token')
    // If I have a token, I fetch the user
    // else I stop here and redirect to the login page
    if (token) {
      me()
    } else {
      setInit(false)
    }
  }, [])

  // Launch the fetchUser function when the component is mounted
  useEffect(() => {
    fetchUser()
  }, [])

  // If I receive data from the "me" query, I set the user
  useEffect(() => {
    if (data) {
      setUser(data.me)
    }
  }, [data])

  // I check if the user is set before redirecting to avoid ui to flicker.
  // setState being asynchrone
  useEffect(() => {
    if (user) {
      setInit(false)
    }
  }, [user])

  // If I receive an error, I remove the token from the localStorage
  // and it will then be handle by the PrivateRoute/PublicRoute component
  useEffect(() => {
    if (error) {
      localStorage.removeItem('token')
      setInit(false)
    }
  }, [error])

  return { init }
}

```

But for this to work, I will have to set the token as an **authorization header**. Therefore, I need to change my client a little bit.

*src/client/index.ts*

```typescript
import { ApolloClient, from, HttpLink, InMemoryCache } from '@apollo/client'
import { setContext } from '@apollo/client/link/context'

// I add the token as an authorization header
const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem('token')

  if (token) {
    return {
      headers: {
        authorization: 'Bearer ' + token,
      },
    }
  }
})

const httpLink = new HttpLink({
  uri: process.env.REACT_APP_BACKEND_URL || 'http://localhost:4000',
})

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache(),
})

export default client

```



*src/graphql/auth/queries.ts*

```typescript
import { gql } from '@apollo/client'

export const ME = gql`
  query {
    me {
      id
      username
      display_name
      email
      created_at
      updated_at
    }
  }
`

```



I think I talked about pretty much everything I did to implement this simple authentication. If you have some questions, feel free to ask ;)

Bye everyone ;).

Take care! ;)