DEV Community

Muhammad Talha Akbar
Muhammad Talha Akbar

Posted on

ReactJS and Laravel - Running through a basic setup - Part 2

In this big article, we will use Laravel and ReactJS to setup authentication system. We'll use Auth scaffolding that comes with Laravel 5.5. Anyways, before we start running through the code, it'll be a good idea to divide our goal, the authentication system, into smaller parts:

  • Landing Page
  • Sign In Page
  • Create Account Page
  • Dashboard Page

With the idea above, we can be sure that we'll have four components: SignInPage, CreateAccountPage, LandingPage, DashboardPage. And let's say these components will be shown on /signin, /account/create, / and /dashboard URLs respectively. Considering this, we can quickly predict how our router components will look like:

<BrowserRouter>
    <div>
        <Route exact path="/" component={LandingPage} />
        <Route path="/signin" component={SignInPage} />
        <Route path="/account/create" component={CreateAccountPage} />
        <Route path="/dashboard" component={DashboardPage} />
    </div>
</BrowserRouter>
Enter fullscreen mode Exit fullscreen mode

Before you ask, the <div> becomes necessary because <BrowserRouter> accepts only one child. Continuing, let's make the components:

const LandingPage = (props) => {
    return <div>Landing Page</div>;
};
const SignInPage = (props) => {
    return <div>Sign In Page</div>;
};
const CreateAccountPage = (props) => {
    return <div>Create Account Page</div>;
};
const DashboardPage = (props) => {
    return <div>Dashboard Page</div>;
};
Enter fullscreen mode Exit fullscreen mode

We've kept them simple; so far. Also, notice all of the four components are stateless. It's my little thing that I always start off with stateless components and then if needed make them stateful. Unless, ofcourse, you know already that your component will have state. Anyways, we move on.

If you haven't guessed it so far, all of the code above will go into our /resources/assets/js/components/App.js file with a few adjustments, ofcourse. Let's see how App.js should look like so far:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

import {
    BrowserRouter,
    Route
} from 'react-router-dom';

let LandingPage = (props) => {
    return <h1 className="mt-5">Landing Page</h1>;
};
let SignInPage = (props) => {
    return <h1 className="mt-5">Sign In Page</h1>;
};
let CreateAccountPage = (props) => {
    return <h1 className="mt-5">Create Account Page</h1>;
};
let DashboardPage = (props) => {
    return <h1 className="mt-5">Dashboard Page</h1>;
};

class App extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
        <BrowserRouter>
            <div>
                <Route exact path="/" component={LandingPage} />
                <Route path="/signin" component={SignInPage} />
                <Route path="/account/create" component={CreateAccountPage} />
                <Route path="/dashboard" component={DashboardPage} />
            </div>
        </BrowserRouter>
        );
    }
};

ReactDOM.render(<App />, document.getElementById('app'));
Enter fullscreen mode Exit fullscreen mode

And yes, by principle, LandingPage, SignInPage, CreateAccountPage, DashboardPage should be in their own file and should be imported into App.js but, for simplicity, we keep it as is. Next, we can make a Header component that contains Link components. So, let's import Link from react-router-dom and declare Header as follows:

let Header = (props) => {
    return (
        <nav className="navbar navbar-expand-lg navbar-light bg-light">
            <div className="container px-2 px-sm-3">
                <Link className="navbar-brand" to="/">DEV.TO</Link>
                <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                    <span className="navbar-toggler-icon"></span>
                </button>

                <div className="collapse navbar-collapse" id="navbarSupportedContent">
                    <ul className="navbar-nav ml-auto">
                        <li className="nav-item">
                            <Link className="nav-link" to="/signin">Sign In</Link>
                        </li>
                        <li className="nav-item">
                            <Link className="btn btn-primary ml-lg-2 ml-sm-0 mt-2 mt-lg-0" to="/account/create">Create Account</Link>
                        </li>
                    </ul>
                    <ul className="navbar-nav ml-auto">
                        <li className="nav-item">
                            <Link className="btn btn-primary ml-lg-2 ml-sm-0 mt-2 mt-lg-0" to="/dashboard">Dashboard</Link>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    );
};
Enter fullscreen mode Exit fullscreen mode

I have kept the Dashboard link in a separate <ul> because soon we will introduce a conditional if/else in the form of ternary operator in our Header. But, let's keep running!

I desperately want to skip to SignInPage component but, let's look at the App.js file so far:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

import {
    BrowserRouter,
    Link,
    Route
} from 'react-router-dom';

let LandingPage = (props) => {
    return <h1 className="mt-5">Landing Page</h1>;
};
let SignInPage = (props) => {
    return <h1 className="mt-5">Sign In Page</h1>;
};
let CreateAccountPage = (props) => {
    return <h1 className="mt-5">Create Account Page</h1>;
};
let DashboardPage = (props) => {
    return <h1 className="mt-5">Dashboard Page</h1>;
};
let Header = (props) => {
    return (
        <nav className="navbar navbar-expand-lg navbar-light bg-light">
            <div className="container px-2 px-sm-3">
                <Link className="navbar-brand" to="/">DEV.TO</Link>
                <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                    <span className="navbar-toggler-icon"></span>
                </button>

                <div className="collapse navbar-collapse" id="navbarSupportedContent">
                    <ul className="navbar-nav ml-auto">
                        <li className="nav-item">
                            <Link className="nav-link" to="/signin">Sign In</Link>
                        </li>
                        <li className="nav-item">
                            <Link className="btn btn-primary ml-lg-2 ml-sm-0 mt-2 mt-lg-0" to="/account/create">Create Account</Link>
                        </li>
                    </ul>
                    <ul className="navbar-nav ml-auto">
                        <li className="nav-item">
                            <Link className="btn btn-primary ml-lg-2 ml-sm-0 mt-2 mt-lg-0" to="/dashboard">Dashboard</Link>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    );
};

class App extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
        <BrowserRouter>
            <div>
                <Header />
                <Route exact path="/" component={LandingPage} />
                <Route path="/signin" component={SignInPage} />
                <Route path="/account/create" component={CreateAccountPage} />
                <Route path="/dashboard" component={DashboardPage} />
            </div>
        </BrowserRouter>
        );
    }
};

ReactDOM.render(<App />, document.getElementById('app'));
Enter fullscreen mode Exit fullscreen mode

Now, SignInPage is going to contain a form mainly. And, for the form management, we have included redux-form which depends on redux. Let's quickly setup both:

...

import { createStore, combineReducers } from 'redux'
import { reducer as formReducer } from 'redux-form'

...

const rootReducer = combineReducers({
  form: formReducer,
  // my other reducers come here
});


const store = createStore(rootReducer);
Enter fullscreen mode Exit fullscreen mode

Now, we have a Redux store but to actually use this store with React components, we'll need to use Provider component that comes with react-redux package. It's quite simple (we just need to wrap our BrowserRouter with Provider and pass the store as a prop) so, let's just look at our App.js so far:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

import {
    BrowserRouter,
    Link,
    Route
} from 'react-router-dom';

import { createStore, combineReducers } from 'redux'
import { reducer as formReducer } from 'redux-form'

import { Provider} from 'react-redux'

// Other components are here collapsed

const rootReducer = combineReducers({
  form: formReducer,
  // my other reducers come here
});

const store = createStore(rootReducer);

class App extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
        <Provider store={store}>
            <BrowserRouter>
                <div>
                    <Header />
                    <Route exact path="/" component={LandingPage} />
                    <Route path="/signin" component={SignInPage} />
                    <Route path="/account/create" component={CreateAccountPage} />
                    <Route path="/dashboard" component={DashboardPage} />
                </div>
            </BrowserRouter>
        </Provider>
        );
    }
};

ReactDOM.render(<App />, document.getElementById('app'));
Enter fullscreen mode Exit fullscreen mode

Now, let's modify the SignInPage component:

class SignInPage extends Component {
    constructor(props) {
        super(props);

        this.processSubmit = this.processSubmit.bind(this);
    }

    componentWillMount() {
        // do something like setting default state
    }

    processSubmit(values) {
        // do something with the values
    }

    render() {
        const { handleSubmit, submitting } = this.props;

        return (
            <div className="container mt-5">
                <div className="row justify-content-center">
                    <div className="col-6">
                        <div className="card">
                            <div className="card-body">
                            <h2 className="text-center font-weight-light mb-4">Sign into your account</h2>
                                <form onSubmit={handleSubmit(this.processSubmit)}>
                                    <Field
                                        label="Email Address"
                                        name="email"
                                        component={FormField}
                                        id="email"
                                        type="text"
                                        className="form-control"
                                    />
                                    <Field label="Password" name="password" component={FormField} id="password" type="password" className="form-control" />
                                    <div className="form-check">
                                        <label className="form-check-label">
                                            <Field name="remember" component="input" type="checkbox" className="form-check-input mt-2" value="1" />
                                            Remember me
                                        </label>
                                    </div>
                                    <div className="form-group mt-4">
                                        <button type="submit" className="btn btn-secondary" disabled={submitting}>Continue</button>
                                    </div>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
};

SignInPage = reduxForm({
    form: 'signin',
    validate
})(SignInPage);
Enter fullscreen mode Exit fullscreen mode

There a few things here to digest before we move on:

  • SignInPage component is no longer a pathetic dumb component that just renders the other components and pass props.
  • The reduxForm decorator and Field component has been imported from redux-form package so, our import declaration for redux-form now looks like this:
import {
    reducer as formReducer,
    reduxForm,
    Field
} from 'redux-form'
Enter fullscreen mode Exit fullscreen mode
  • Notice the Field takes a component as a prop. Any component that you specify will be rendered inside Field. The inner component that we specify gets some nice props that can be used to make the inputs interactive. Here's how I have written a basic FormField component:
const FormField = ({
        label,
        input,
        type,
        name,
        className,
        meta: { touched, error, warning }
}) => (
    <div className="form-group">
        {
            label &&
            <label htmlFor={name}>{label}</label>
        }
        <input {...input } name={name} type={type} className={
            `${className} ${
                touched && (
                    (error && 'is-invalid')
                )
            }`
        } />
        {
            touched &&
                (error && <span className="invalid-feedback">{error}</span>)
        }
    </div>
);
Enter fullscreen mode Exit fullscreen mode
  • Look at this line carefully in the new SignInPage component:
const { handleSubmit, submitting } = this.props;
Enter fullscreen mode Exit fullscreen mode

The handleSubmit and submitting props come from the decorated SignInPage component . handleSubmit takes a callback with values being passed as its first parameter. submitting is just a boolean that tells if the form is in submitting state which is helpful in disabling the submit button to prevent multiple requests when the one is being processed.

  • Also, notice validate property in the following lines:
SignInPage = reduxForm({
    form: 'signin',
    validate: validatorSignInForm,
})(SignInPage);
Enter fullscreen mode Exit fullscreen mode

The validate property must hold a function that returns error messages for fields. I'll be using a simple JavaScript based validation library validate.js to implement validatorSignInForm which you can install with npm install --save validate.js. The validator function looks like below:

const validatorSignInForm = (values) => {
    const result = validate(values, {
        email: {
            presence: {
                message: '^Please enter your email address.'
            },
            email: {
                message: '^Please enter a valid email address.'
            }
        },
        password: {
            presence: {
                message: '^Please enter your password.'
            }
        }
    });

    return result;
};
Enter fullscreen mode Exit fullscreen mode

You should know that validate() comes from validate.js library so, for now you just need to know that it takes values as its first argument and rules for each value in nicely formed object as its second argument.

Now, that's a lot of code that we have looked over so far but, you'll see that once you have gone through and understood it, you'll encounter no problem creating interactive forms for your application.

Now, let's put the updated SignInPage component along with the code we've written in App.js so far and see how it looks like now:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

import {
    BrowserRouter,
    Link,
    Route
} from 'react-router-dom';

import { createStore, combineReducers } from 'redux'
import {
    reducer as formReducer,
    reduxForm,
    Field
} from 'redux-form'

import { Provider} from 'react-redux'

// Other components are here collapsed

const validatorSignInForm = (values) => {
    const result = validate(values, {
        email: {
            presence: {
                message: '^Please enter your email address.'
            },
            email: {
                message: '^Please enter a valid email address.'
            }
        },
        password: {
            presence: {
                message: '^Please enter your password.'
            }
        }
    });

    return result;
};

class SignInPage extends Component {
    constructor(props) {
        super(props);

        this.processSubmit = this.processSubmit.bind(this);
    }

    componentWillMount() {
        // do something like setting default state
    }

    processSubmit(values) {
        // do something with the values
    }

    render() {
        const { handleSubmit, submitting } = this.props;

        return (
            <div className="container mt-5">
                <div className="row justify-content-center">
                    <div className="col-6">
                        <div className="card">
                            <div className="card-body">
                            <h2 className="text-center font-weight-light mb-4">Sign into your account</h2>
                                <form onSubmit={handleSubmit(this.processSubmit)}>
                                    <Field
                                        label="Email Address"
                                        name="email"
                                        component={FormField}
                                        id="email"
                                        type="text"
                                        className="form-control"
                                    />
                                    <Field label="Password" name="password" component={FormField} id="password" type="password" className="form-control" />
                                    <div className="form-check">
                                        <label className="form-check-label">
                                            <Field name="remember" component="input" type="checkbox" className="form-check-input mt-2" value="1" />
                                            Remember me
                                        </label>
                                    </div>
                                    <div className="form-group mt-4">
                                        <button type="submit" className="btn btn-secondary" disabled={submitting}>Continue</button>
                                    </div>
                                </form>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
};

SignInPage = reduxForm({
    form: 'signin',
    validate: validatorSignInForm
})(SignInPage);

const rootReducer = combineReducers({
  form: formReducer,
  // my other reducers come here
});

const store = createStore(rootReducer);

class App extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
        <Provider store={store}>
            <BrowserRouter>
                <div>
                    <Header />
                    <Route exact path="/" component={LandingPage} />
                    <Route path="/signin" component={SignInPage} />
                    <Route path="/account/create" component={CreateAccountPage} />
                    <Route path="/dashboard" component={DashboardPage} />
                </div>
            </BrowserRouter>
        </Provider>
        );
    }
};

ReactDOM.render(<App />, document.getElementById('app'));
Enter fullscreen mode Exit fullscreen mode

Break - Breath a little

So far, we have looked at a lot of code. You must be overwhelmed. Take a break. Inhale air. And be back where we start looking at processing the sign in form.

So, have you noticed that I had left processSubmit method of SignInPage component empty? Our goal will now be to look at how we will implement it. A break-down of its implementation will be:

  • Send an AJAX request to the login route with the values
  • Wait for the response
  • Do something upon the request results

Now, for sending AJAX requests, we will use axios - a promise based AJAX library. Don't worry, it's already installed. You can start using it like below:

processSubmit(values) {
    axios
    .post('/ajax/login', values)
    .then( (response) => {
        console.log(response.data);
    })
    .error( (err) => {
        console.log(err);
    });
}
Enter fullscreen mode Exit fullscreen mode

A few things to setup before you can see the request completing successfully:

  • You need to put Auth::routes(); in ajax Route group that we created in Part 1 in /routes/web.php.
  • You need to make sure that you have <meta> tag in your web.blade.php that contains the CSRF token otherwise, Laravel will throw TokenMismatchException instead of processing the login request. So make sure something like below is there:
<meta name="csrf-token" content="{{ csrf_token() }}">
Enter fullscreen mode Exit fullscreen mode
  • By default, AuthenticatesUsers trait in LoginController has a method sendLoginResponse that is implemented to redirect. The problem is that we don't want to redirect instead, we want to return user details. So, copy the following lines of code and paste them as methods of LoginController which resides in /app/Http/Controllers/Auth/LoginController.php:
/**
 * Send the response after the user was authenticated.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
protected function sendLoginResponse(Request $request)
{
    $request->session()->regenerate();

    $this->clearLoginAttempts($request);

    $user = $this->guard()->user();

    if($this->authenticated($request, $user)) {
        return response()->json([
            'success' => true,
            'user' => $user
        ], 200);
    }
}

/**
 * The user has been authenticated.
 *
 * @param  \Illuminate\Http\Request  $request
 * @param  mixed  $user
 * @return mixed
 */
protected function authenticated(Request $request, $user)
{
    return true;
}
Enter fullscreen mode Exit fullscreen mode
  • Similarly, the AuthenticatesUsers trait has sendFailedLoginResponse which also needs to be overridden. So, copy the below lines into LoginController:
/**
 * Get the failed login response instance.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Symfony\Component\HttpFoundation\Response
 */
protected function sendFailedLoginResponse(Request $request)
{
    return response()->json([
        'success' => false,
        'message' => trans('auth.failed')
    ], 422);
}
Enter fullscreen mode Exit fullscreen mode

Now, if you test by submitting the form, you'll be able to receive the JSON responses. Or it doesn't? Let me know.

To be continued in Part 3. In Part 3, will continue with processSubmit for the SignInPage component. And, we will also look at how we can manage the user details using redux, prevent routes from being accessed in guest mode and much more.

The reason I have to split this into part 3 is because the dev.to post editor has become slow due to large amount of content in it and it's annoying!

Top comments (9)

Collapse
 
lorcan profile image
Lorcan

Hi

Thanks for this tutorial. I'm getting "View [auth.login] not found." on the login option, and similar for the Register option.

Any idea why that might be?

Thanks

Collapse
 
devtalhaakbar profile image
Muhammad Talha Akbar

In your routes file, change this:
Route::get('/{path?}', function () {
return view('web');
});
to this:
Route::get('/{path?}', function () {
return view('web')->with('user', Auth::user());
})->where('path', '.*');

Collapse
 
lorcan profile image
Lorcan

Ok. Tried that, didn't seem to make any difference

Thread Thread
 
devtalhaakbar profile image
Muhammad Talha Akbar

Can you show exactly how your routes file look like? You might want to place Auth::routes(); in the /ajax block.

Thread Thread
 
lorcan profile image
Lorcan

routes/web.php -

<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::group(['prefix' => 'ajax'], function() {
// all routes that don't need to go to react-router
Auth::routes();
});

Route::get('/{path?}', function () {
return view('web')->with('user', Auth::user());
})->where('path', '.*');

Collapse
 
maximilliancode profile image
Maximillian

Please post the part 3.. Your post is very helpful.. Thanks

Collapse
 
arifr007 profile image
Arif Ramadan

where part 3? T.T

Collapse
 
bvipul profile image
Vipul Basapati

there is just problem, with axios, there is no error method on it, instead we need to use catch method. Other than that, awesome content.

Collapse
 
bvipul profile image
Vipul Basapati

Awesome content, when will the next part will be published? Thanks for sharing.