Skip to Content
Menu
All Posts

Article

Setting Up Laravel and React.js the Right Way (Using User Authentication) II

Back-end https://toluwaniadewale.com/blog/setting-up-laravel-and-reactjs-the-right-way-using-user-authenticationFront-endWe're at the easier part now, or are we? 😅 In this section, we will be crea...

Toluwani Adewale 11 min read
Updated

Back-end 

https://toluwaniadewale.com/blog/setting-up-laravel-and-reactjs-the-right-way-using-user-authentication

 

Front-end

We're at the easier part now, or are we? 😅 In this section, we will be creating views for registration, login, and dashboard using React. We'll make use of:

  1. TypeScript - React
  2. A bunch of node modules 💀

 

Setting Up TypeScript

There is a range of ways to download TypeScript into our project, the one we'll focus on in this article is on a per-project basis, i.e. having Typescript accessible within our project as a package.

Recall Sail? node and npm already ship into Laravel Sail, this makes local installation a breeze. We just run the command below:

sail npm install typescript --save-dev

The command above also goes ahead to download a couple of very important packages that we'll discuss and use going forward. These packages are found in the devDependencies of our package.json; axios, laravel-mix, and postcss.

We need some sort of configuration file to help TypeScript properly compile our React project to JavaScript and communicate with webpack (which we'll discuss soon), we'll do this by generating a tsconfig.json file with the command below:

sail npx tsc --init --jsx react

 

Setting Up React

To finally install React, we'll run the following command:

sail npm install react react-dom react-router-dom @types/react @types/react-dom @types/react-router-dom

The beauty of Laravel is its simplicity, the framework makes development easy; the developers do exactly the same - there's basically a package for everything 😅. Laravel Mix was developed by Jeffrey Way to abstract all of the complexities of setting up webpack in our Laravel projects. Webpack in very simple terms basically bundles all of our frontend assets into a single or more files. It's like mashing all our JavaScript and CSS files into one larger file - or more, depending on our configurations.

Before we delve into setting up Mix, there's one more set of devDependencies installations we need to go. We'll run the following and go over what they imply:

sail npm install ts-loader @babel/preset-react @babel/preset-typescript @babel/preset-env --dev

ts-loader helps webpack communicate with TypeScript (kinda like what tsconfig.json file does but in reverse). @babel/preset-react on the other hand, basically adds support for React's JSX syntax, it converts JSX to JavaScript in our browser. @babel/preset-typescript simply converts TypesScript to JavaScript; TSX to JSX. @babel/preset-env enables us to use the latest JavaScript syntaxes in our browsers. See ehn, the JavaScript ecosystem is something else 💀.

There's currently no way for Mix (webpack) to communicate with all those @babel/... stuff we've installed up there so we've got to create a config file for that named .babelrc in our root folder (where tsconfig.json resides) with the content:

{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react",
        "@babel/preset-typescript"
    ],
}

We'll then proceed by updating our webpack.mix.js file which contains configurations for Mix. Here we change app.js to app.tsx, then we tell Mix to resolve all files that have extensions .tsx and .ts. We're simply telling Mix to find these files so we can actually properly import them as modules. Lastly, we attach a method called react()  to the chain to tell Mix that we'd be making use of React. It all ends up looking like this:

mix.js("resources/js/app.tsx", "public/js")
    .postCss("resources/css/app.css", "public/css", [
        //
    ])
    .webpackConfig({
        resolve: {
            extensions: [".ts", ".tsx"],
        },
    })
    .react();

To corroborate this change, we also rename the file in resources/js/app.js to resources/js/app.tsx, then we run the following to bundle and compile our assets and continue watching for changes to our code in order to update our bundle:

sail npm run watch

Finally, we will remove everything within the <html></html> of our resources\views\welcome.blade.php and replace with the following:

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Laravel Typescript React Authentication</title>

    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>

<body>
    <div id="index"></div>
    <script src="{{ asset('js/app.js') }}"></script>
</body>

The important change to note here is that we're importing our compiled assets css/app.css and js/app.js into our single entry blade file which we had previously routed all URLs to in the backend section. This as you might already know renders our index view with React components.

 

Setting Up Our Structure

Our front end will be structured somewhat differently from what we had in the older article. Our directory structure placed in resources/js  is presented below:

.
├── Router.tsx
├── app.tsx
├── components/
│   ├── Footer.tsx
│   ├── Header.tsx
│   └── types/
├── http-common.ts
├── layouts/
│   └── Master.tsx
├── service/
│   ├── Auth.service.ts
│   ├── Sanctum.service.ts
│   └── types/
│       └── AuthData.ts
├── utils/
└── views/
    ├── Home.tsx
    ├── Login.tsx
    ├── NotFound.tsx
    ├── Register.tsx
    └── user/
        └── Dashboard.tsx

 

Router.tsx

import React from "react";
import { Routes, Route } from "react-router-dom";

import Master from "./layouts/Master";
import Home from "./views/Home";
import Login from "./views/Login";
import Register from "./views/Register";
import Dashboard from "./views/user/Dashboard";
import NotFound from "./views/NotFound";

export function App(): JSX.Element {
    return (
        <Routes>
            <Route path="/" element={<Master />}>
                <Route index element={<Home />} />
                <Route path="/login" element={<Login />} />
                <Route path="/register" element={<Register />} />
                {/* Authenticated */}
                <Route path="/dashboard" element={<Dashboard />} />
            </Route>
            <Route path="*" element={<NotFound />} />
        </Routes>
    );
}

We import our view components and create routes for them.

TIP: The files for the views above will be created in the segments going forward so you could actually comment out non-existing files if you choose to properly follow along.

 

app.tsx

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import { App } from "./Router";

ReactDOM.render(
    <React.StrictMode>
        <BrowserRouter>
            <App />
        </BrowserRouter>
    </React.StrictMode>,
    document.getElementById("index")
);

Nah, I'm not explaining this one 💀.

 

components/Footer.tsx

import React from "react";

const Footer = (): JSX.Element => {
    return <footer>Whatever normally goes into the footer</footer>;
};

export default Footer;

This one too.

 

components/Header.tsx

import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import AuthService from "../service/Auth.service";
import {
    useNavigate,
    NavigateFunction,
    useLocation,
    Location,
} from "react-router-dom";

const Header = (): JSX.Element => {
    let [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
    let navigate: NavigateFunction = useNavigate();
    let location: Location = useLocation();

    useEffect(() => {
        // 1.1
        AuthService.getUser()
            .then((response) => {
                // 1.2
                if (
                    location.pathname === "/login" ||
                    location.pathname === "/register"
                ) {
                    navigate("/dashboard");
                }

                setIsLoggedIn(true);
            })
            .catch((e) => {
                // 1.3
                if (
                    e.response.data.message === "Unauthenticated." &&
                    location.pathname === "/dashboard"
                ) {
                    navigate("/login");
                }

                setIsLoggedIn(false);
            });
    }, []);

    return (
        <nav>
            <ul>
                <li>
                    <Link to="/">Index</Link>
                </li>
                {isLoggedIn ? (
                    <li>
                        <Link to="/dashboard">Dashboard</Link> |{" "}
                        <Link to="/logout">Logout</Link>
                    </li>
                ) : (
                    <li>
                        <Link to="/login">Login</Link> |{" "}
                        <Link to="/register">Register</Link>
                    </li>
                )}
            </ul>
        </nav>
    );
};

export default Header;

1.1: We send a request to the Get User API Endpoint through the AuthService which we'll create soon.

1.2: We always redirect Authenticated users to the dashboard page if they try to access either the login or register page.

1.3: We always redirect Unauthenticated users to the login page if they try to access the dashboard page.

This Authentication Guard could be moved to a PrivateRoute or handled more gracefully as we did in our older article, I'm pretty lazy and exhausted at this point 💀. I plan to write a Redux tutorial to cover this using middleware 😅. 

 

http-common.ts

import axios from "axios";

export default axios.create({
    baseURL: process.env.MIX_APP_URL,
    headers: {
        "Content-type": "application/json",
    },
});

We create initialize and configure axios to be used in our services.

Mix allows us to use variables from .env in our React project by prefixing the keys with MIX_. We ensure to add our domain to the key MIX_APP_URL at the bottom of .env like this:

MIX_APP_URL=http://laravel-typescript-react-authentication.test

 

layouts/Master.tsx

import React from "react";

import { Outlet } from "react-router-dom";

import Header from "../components/Header";
import Footer from "../components/Footer";

const Master = (): JSX.Element => {
    return (
        <div>
            <Header />
            <main>
                <section>
                    <Outlet />
                </section>
            </main>
            <Footer />
        </div>
    );
};

export default Master;

Master is our default view layout that nests other views, it's where we import our Header and Footer components that are common to all views. Outlet basically returns views for matched routes. Like Laravel's layout, this is very useful when you want a different layout for some other page, perhaps a page that doesn't need a footer.

 

service/Auth.service.ts

import { AxiosResponse } from "axios";
import http from "../http-common";
import SanctumService from "./Sanctum.service";
import { LoginData } from "./types/AuthData";
import { RegisterData } from "./types/AuthData";

class AuthService {
    constructor(public prefix = "") {
        this.prefix = "/api";
    }

    register(data: RegisterData): Promise<AxiosResponse<string>> {
        return http.post(`${this.prefix}/register`, data);
    }

    login(data: LoginData): Promise<AxiosResponse<any>> {
        return SanctumService.csrfProtection().then((response: AxiosResponse) =>
            http.post(`${this.prefix}/login`, data)
        );
    }

    getUser(): Promise<AxiosResponse<any>> {
        return http.get(`${this.prefix}/user`);
    }

    logout(): Promise<AxiosResponse<any>> {
        return http.post(`${this.prefix}/logout`);
    }
}

export default new AuthService();

We define methods to call our defined Authentication API endpoints using axios.

 

service/Sanctum.service.ts

import { AxiosResponse } from "axios";
import http from "../http-common";

class SanctumService {
    csrfProtection(): Promise<AxiosResponse<any>> {
        return http.get("/sanctum/csrf-cookie");
    }
}

export default new SanctumService();

We define a method to call our defined Sanctum API endpoint using axios.

 

service/types/LoginData.ts

export type LoginData = {
    email: string;
    password: string;
};

export type RegisterData = {
    name: string;
    email: string;
    password: string;
    password_confirmation: string;
};

export type UserData = {
    id: string;
    name: string;
    email: string;
};

 

views/Home.tsx

import React from "react";

const Home = (): JSX.Element => {
    return (
        <>
            Whatever normally goes into the home/index page; A Plea To Heal The
            World for instance
        </>
    );
};

export default Home;

 

views/Login.tsx

import React, { useState } from "react";
import { Link, useNavigate, NavigateFunction } from "react-router-dom";
import AuthService from "../service/Auth.service";

const Login = (): JSX.Element => {
    let [isFormSubmitting, setIsFormSubmitting] = useState<boolean>(false);
    let [error, setError] = useState<string>();
    let [email, setEmail] = useState<string>("");
    let [password, setPassword] = useState<string>("");
    let navigate: NavigateFunction = useNavigate();

    let handleSubmit = (e: React.ChangeEvent<HTMLFormElement>) => {
        e.preventDefault();
        setIsFormSubmitting(true);
        AuthService.login({ email, password })
            .then((response) => {
                navigate("/dashboard");
            })
            .catch((e) => {
                setError(e.response.data.message);
                setIsFormSubmitting(false);
            });
    };

    return (
        <div>
            <div>
                <div>
                    <h2>Log In To Your Account</h2>
                    {error ? <h5>Error: {error}</h5> : ""}
                    <form onSubmit={handleSubmit}>
                        <div>
                            <input
                                id="email"
                                type="email"
                                name="email"
                                placeholder="E-mail"
                                required
                                value={email}
                                onChange={(e) => {
                                    setEmail(e.target.value);
                                }}
                            />
                        </div>
                        <div>
                            <input
                                id="password"
                                type="password"
                                name="password"
                                placeholder="Password"
                                required
                                onChange={(e) => {
                                    setPassword(e.target.value);
                                }}
                            />
                        </div>
                        <button disabled={isFormSubmitting} type="submit">
                            {isFormSubmitting ? "Logging You In..." : "Log In"}
                        </button>
                    </form>
                </div>
                <p>
                    Don't have an account? <Link to="/register"> Register</Link>
                    <br />
                    <span>
                        <Link to="/">Back to Index</Link>
                    </span>
                </p>
            </div>
        </div>
    );
};

export default Login;

 

views/NotFound.tsx

import React from "react";
import { Link } from "react-router-dom";

const NotFound = (): JSX.Element => {
    return (
        <p>
            Whatever you were looking for isn't here.{" "}
            <Link to="/">Back to Index</Link>
        </p>
    );
};

export default NotFound;

 

views/Register.tsx

import React, { useState } from "react";
import { Link, useNavigate, NavigateFunction } from "react-router-dom";
import AuthService from "../service/Auth.service";

const Register = (): JSX.Element => {
    let [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
    let [isFormSubmitting, setIsFormSubmitting] = useState<boolean>(false);
    let [error, setError] = useState<string>();
    let [email, setEmail] = useState<string>("");
    let [name, setName] = useState<string>("");
    let [password_confirmation, setPasswordConfirmation] = useState<string>("");
    let [password, setPassword] = useState<string>("");
    let navigate: NavigateFunction = useNavigate();

    let handleSubmit = (e: React.ChangeEvent<HTMLFormElement>) => {
        e.preventDefault();
        setIsFormSubmitting(true);
        AuthService.register({
            name,
            email,
            password,
            password_confirmation,
        })
            .then((response) => {
                navigate("/login");
            })
            .catch((e) => {
                setError(e.response.data.message);
                setIsFormSubmitting(false);
            });
    };

    return (
        <div>
            <div>
                <div>
                    <h2>Create Your Account</h2>
                    {isLoggedIn ? (
                        <h5>Registration successful, redirecting...</h5>
                    ) : (
                        ""
                    )}
                    {error ? <h5>Error: {error}</h5> : ""}
                    <form onSubmit={handleSubmit}>
                        <div>
                            <input
                                id="name"
                                type="input"
                                name="name"
                                placeholder="Name"
                                required
                                value={name}
                                onChange={(e) => {
                                    setName(e.target.value);
                                }}
                            />
                        </div>
                        <div>
                            <input
                                id="email"
                                type="email"
                                name="email"
                                placeholder="E-mail"
                                required
                                value={email}
                                onChange={(e) => {
                                    setEmail(e.target.value);
                                }}
                            />
                        </div>
                        <div>
                            <input
                                id="password"
                                type="password"
                                name="password"
                                placeholder="Password"
                                required
                                onChange={(e) => {
                                    setPassword(e.target.value);
                                }}
                            />
                        </div>
                        <div>
                            <input
                                id="password_confirmation"
                                type="password"
                                name="password_confirmation"
                                placeholder="Confirm Password"
                                required
                                onChange={(e) => {
                                    setPasswordConfirmation(e.target.value);
                                }}
                            />
                        </div>
                        <button disabled={isFormSubmitting} type="submit">
                            {isFormSubmitting
                                ? "Creating Account..."
                                : "Create Account"}
                        </button>
                    </form>
                </div>
                <p>
                    Already have an account? <Link to="/login"> Login</Link>
                    <br />
                    <span>
                        <Link to="/">Back to Index</Link>
                    </span>
                </p>
            </div>
        </div>
    );
};

export default Register;

 

views/user/Dashboard.tsx

import React, { useState, useEffect } from "react";
import { useNavigate, NavigateFunction } from "react-router-dom";
import AuthService from "../../service/Auth.service";
import { UserData } from "../../service/types/AuthData";

const Dashboard = (): JSX.Element => {
    let [user, setUser] = useState<UserData>();
    let navigate: NavigateFunction = useNavigate();

    useEffect(() => {
        AuthService.getUser()
            .then((response) => {
                setUser(response.data);
            })
            .catch((e) => {
                if (e.response.data.message === "Unauthenticated.") {
                    navigate("/login");
                }
            });
    }, []);

    return (
        <div>
            <span>
                Whatever normally goes into the user dasboard page; the table
                below for instance
            </span>
            <br />
            <table>
                <tbody>
                    <tr>
                        <th scope="row ">User Id</th>
                        <td>{user?.id}</td>
                    </tr>
                    <tr>
                        <th scope="row ">Full Name</th>
                        <td>{user?.name}</td>
                    </tr>
                    <tr>
                        <th scope="row ">Email</th>
                        <td>{user?.email}</td>
                    </tr>
                </tbody>
            </table>
        </div>
    );
};

export default Dashboard;

 

Conclusion

This is just a skeletal structure for user authentication using React.js and Laravel with no styling; you can always add or subtract from it. If you find any bugs or errors in any code snip above, feel free to contact me.
Repository: 
Github

Responses (0)

Have a question, correction, or another approach? Add your response.

Your name and response will be public.

No responses yet.