Build a Login/Auth App with the MERN Stack — Part 3 (Linking Redux with React Co...
source link: https://www.tuicool.com/articles/hit/e2IJ3ay
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Create a (minimal) full-stack app with user authentication via passport and JWTs
Before we get started
Read Part 1: Creating our Backend
Build a Login/Auth App with the MERN Stack — Part 1 (Backend)
Create a (minimal) full-stack app with user authentication via passport and JWTs. blog.bitsrc.io
In Part 1, we
- Initialized our backend using
npm
and install necessary packages - Set up a MongoDB database using mLab
- Set up a server with
Node.js
andExpress
- Created a database schema to define a
User
for registration and login purposes - Set up three API routes,
register
,login
, andcurrentuser
usingpassport
+jsonwebtoken
s for authentication andvalidator
for input validation - Tested our API routes using Postman
Read Part 2: Frontend & Redux Setup
Build a Login/Auth App with the MERN Stack — Part 2 (Frontend & Redux Setup)
Create a (minimal) full-stack app with user authentication via passport and JWTs blog.bitsrc.io
In Part 2, we
create-react-app components Redux
In this final part, we will
Redux React jwt
Part 3: Linking Redux with React Components
1. Linking Redux
to our Register
component and displaying errors in our form
i. Using connect()
from react-redux
connect()
does just that; it connects our React
components to our Redux
store
provided by the Provider
component
We have to modify our export default Register;
at the bottom of Register.js
. Read the connect
documentation for more clarification.
export default connect( mapStateToProps, { registerUser } )(withRouter(Register));
You may also notice we wrapped our Register
with a withRouter()
. While it is easy to redirect within a component
(can simply say this.props.history.push('/dashboard')
for example), we can’t do that by default within an action
. To allow us to redirect within an action
, we
- Used
withRouter
fromreact-router-dom
, wrapping our component in ourexport
withRouter()
- Will add a parameter to
this.props.history
within our call tothis.props.registerUser(newUser, this.props.history)
in ouronSubmit
event so we can easily access it within our action (step iv below)
ii. mapStateToProps
mapStateToProps
allows us to get our state
from Redux
and map it to props
which we can use inside components.
We’ll add the following above our export
at the bottom of Register.js
.
const mapStateToProps = state => ({ auth: state.auth, errors: state.errors });
This allows us to call this.props.auth
or this.props.errors
within our Register
component.
iii. Defining propTypes
Since we cannot define types in our constructor, it is considered good convention to do so using the prop-types
package.
Register.propTypes = { registerUser: PropTypes.func.isRequired, auth: PropTypes.object.isRequired, errors: PropTypes.object.isRequired };
iv. Tying it all together
All said and done, let’s make the following bolded additions to our Register.js
React component. We’ll also display errors within our form here.
import React, { Component } from "react";
import { Link, withRouter } from "react-router-dom";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { registerUser } from "../../actions/authActions";
import classnames from "classnames";
class Register extends Component { constructor() { super(); this.state = { name: "", email: "", password: "", password2: "", errors: {} }; }
componentWillReceiveProps(nextProps) {
if (nextProps.errors) {
this.setState({
errors: nextProps.errors
});
}
}
onChange = e => { this.setState({ [e.target.id]: e.target.value }); };
onSubmit = e => { e.preventDefault();
const newUser = { name: this.state.name, email: this.state.email, password: this.state.password, password2: this.state.password2 };
this.props.registerUser(newUser, this.props.history);
};
render() { const { errors } = this.state;
return (
<div className="container">
<div className="row">
<div className="col s8 offset-s2">
<Link to="/" className="btn-flat waves-effect">
<i className="material-icons left">keyboard_backspace</i> Back to
home
</Link>
<div className="col s12" style={{ paddingLeft: "11.250px" }}>
<h4>
<b>Register</b> below
</h4>
<p className="grey-text text-darken-1">
Already have an account? <Link to="/login">Log in</Link>
</p>
</div>
<form noValidate onSubmit={this.onSubmit}>
<div className="input-field col s12">
<input
onChange={this.onChange}
value={this.state.name}
error={errors.name}
id="name"
type="text"
className={classnames("", {
invalid: errors.name
})}
/>
<label htmlFor="name">Name</label>
<span className="red-text">{errors.name}</span>
</div>
<div className="input-field col s12">
<input
onChange={this.onChange}
value={this.state.email}
error={errors.email}
id="email"
type="email"
className={classnames("", {
invalid: errors.email
})}
/>
<label htmlFor="email">Email</label>
<span className="red-text">{errors.email}</span>
</div>
<div className="input-field col s12">
<input
onChange={this.onChange}
value={this.state.password}
error={errors.password}
id="password"
type="password"
className={classnames("", {
invalid: errors.password
})}
/>
<label htmlFor="password">Password</label>
<span className="red-text">{errors.password}</span>
</div>
<div className="input-field col s12">
<input
onChange={this.onChange}
value={this.state.password2}
error={errors.password2}
id="password2"
type="password"
className={classnames("", {
invalid: errors.password2
})}
/>
<label htmlFor="password2">Confirm Password</label>
<span className="red-text">{errors.password2}</span>
</div>
<div className="col s12" style={{ paddingLeft: "11.250px" }}>
<button
style={{
width: "150px",
borderRadius: "3px",
letterSpacing: "1.5px",
marginTop: "1rem"
}}
type="submit"
className="btn btn-large waves-effect waves-light hoverable blue accent-3"
>
Sign up
</button>
</div>
</form>
</div>
</div>
</div>
);
}
}
Register.propTypes = {
registerUser: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth,
errors: state.errors
});
export default connect(
mapStateToProps,
{ registerUser }
)(withRouter(Register));
ii. Linking Redux
to our Login
component and displaying errors in our form
Let’s make the following bolded additions to our Login.js
React component.
import React, { Component } from "react";
import { Link } from "react-router-dom";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { loginUser } from "../../actions/authActions";
import classnames from "classnames";
class Login extends Component { constructor() { super(); this.state = { email: "", password: "", errors: {} }; }
componentWillReceiveProps(nextProps) {
if (nextProps.auth.isAuthenticated) {
this.props.history.push("/dashboard"); // push user to dashboard when they login
}
if (nextProps.errors) {
this.setState({
errors: nextProps.errors
});
}
}
onChange = e => { this.setState({ [e.target.id]: e.target.value }); };
onSubmit = e => { e.preventDefault();
const userData = { email: this.state.email, password: this.state.password };
this.props.loginUser(userData); // since we handle the redirect within our component, we don't need to pass in this.props.history as a parameter
};
render() { const { errors } = this.state;
return (
<div className="container">
<div style={{ marginTop: "4rem" }} className="row">
<div className="col s8 offset-s2">
<Link to="/" className="btn-flat waves-effect">
<i className="material-icons left">keyboard_backspace</i> Back to
home
</Link>
<div className="col s12" style={{ paddingLeft: "11.250px" }}>
<h4>
<b>Login</b> below
</h4>
<p className="grey-text text-darken-1">
Don't have an account? <Link to="/register">Register</Link>
</p>
</div>
<form noValidate onSubmit={this.onSubmit}>
<div className="input-field col s12">
<input
onChange={this.onChange}
value={this.state.email}
error={errors.email}
id="email"
type="email"
className={classnames("", {
invalid: errors.email || errors.emailnotfound
})}
/>
<label htmlFor="email">Email</label>
<span className="red-text">
{errors.email}
{errors.emailnotfound}
</span>
</div>
<div className="input-field col s12">
<input
onChange={this.onChange}
value={this.state.password}
error={errors.password}
id="password"
type="password"
className={classnames("", {
invalid: errors.password || errors.passwordincorrect
})}
/>
<label htmlFor="password">Password</label>
<span className="red-text">
{errors.password}
{errors.passwordincorrect}
</span>
</div>
<div className="col s12" style={{ paddingLeft: "11.250px" }}>
<button
style={{
width: "150px",
borderRadius: "3px",
letterSpacing: "1.5px",
marginTop: "1rem"
}}
type="submit"
className="btn btn-large waves-effect waves-light hoverable blue accent-3"
>
Login
</button>
</div>
</form>
</div>
</div>
</div>
);
}
}
Login.propTypes = {
loginUser: PropTypes.func.isRequired,
auth: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth,
errors: state.errors
});
export default connect(
mapStateToProps,
{ loginUser }
)(Login);
Right now, when the user logs in, the app redirects us back to a blank page "/dashboard"
per the first conditional statement of our componentWillReceiveProps(nextProps)
lifecycle method. Next, we’ll create our Dashboard
component and make it a PrivateRoute
so that only a logged in user can view it.
Creating our Dashboard component for when users log in
In our component
directory, let’s create a dashboard
directory and within it, a Dashboard.js
file.
➜ <strong>components</strong> mkdir dashboard && cd dashboard && touch Dashboard.js
Let’s place the following in our Dashboard.js
file.
import React, { Component } from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; import { logoutUser } from "../../actions/authActions";
class Dashboard extends Component { onLogoutClick = e => { e.preventDefault(); this.props.logoutUser(); };
render() { const { user } = this.props.auth;
return ( <div style={{ height: "75vh" }} className="container valign-wrapper"> <div className="row"> <div className="col s12 center-align"> <h4> <b>Hey there,</b> {user.name.split(" ")[0]} <p className="flow-text grey-text text-darken-1"> You are logged into a full-stack{" "} <span style={{ fontFamily: "monospace" }}>MERN</span> app :clap: </p> </h4> <button style={{ width: "150px", borderRadius: "3px", letterSpacing: "1.5px", marginTop: "1rem" }} onClick={this.onLogoutClick} className="btn btn-large waves-effect waves-light hoverable blue accent-3" > Logout </button> </div> </div> </div> ); } }
Dashboard.propTypes = { logoutUser: PropTypes.func.isRequired, auth: PropTypes.object.isRequired };
const mapStateToProps = state => ({ auth: state.auth });
export default connect( mapStateToProps, { logoutUser } )(Dashboard);
Creating Protected Routes
There is no standard way of creating protected routes in React
. We’ll use the logic described in the below post by Tyler McGinnis to create authenticated routes (routes that only certain users can access based on their auth status).
In our components
directory, let’s create a directory and file for our private route.
➜ <strong>components</strong> mkdir private-route && cd private-route && touch PrivateRoute.js
Let’s place the following in PrivateRoute.js
.
import React from "react"; import { Route, Redirect } from "react-router-dom"; import { connect } from "react-redux"; import PropTypes from "prop-types";
const PrivateRoute = ({ component: Component, auth, ...rest }) => ( <Route {...rest} render={props => auth.isAuthenticated === true ? ( <Component {...props} /> ) : ( <Redirect to="/login" /> ) } /> );
PrivateRoute.propTypes = { auth: PropTypes.object.isRequired };
const mapStateToProps = state => ({ auth: state.auth });
export default connect(mapStateToProps)(PrivateRoute);
Tying it all together in App.js
In this, we will
- Check
localStorage
for a token to keep the user logged in even if they close or refresh the app (e.g. until they log out or the token expires) - Pull in our
Dashboard
component and define it as aPrivateRoute
Make the following bolded additions to App.js
.
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import jwt_decode from "jwt-decode";
import setAuthToken from "./utils/setAuthToken";
import { setCurrentUser, logoutUser } from "./actions/authActions";
import { Provider } from "react-redux"; import store from "./store";
import Navbar from "./components/layout/Navbar";
import Landing from "./components/layout/Landing";
import Register from "./components/auth/Register";
import Login from "./components/auth/Login";
import PrivateRoute from "./components/private-route/PrivateRoute";
import Dashboard from "./components/dashboard/Dashboard";
// Check for token to keep user logged in
if (localStorage.jwtToken) {
// Set auth token header auth
const token = localStorage.jwtToken;
setAuthToken(token);
// Decode token and get user info and exp
const decoded = jwt_decode(token);
// Set user and isAuthenticated
store.dispatch(setCurrentUser(decoded));
// Check for expired token
const currentTime = Date.now() / 1000; // to get in milliseconds
if (decoded.exp < currentTime) {
// Logout user
store.dispatch(logoutUser());
// Redirect to login
window.location.href = "./login";
}
}
class App extends Component {
render() {
return (
<Provider store={store}>
<Router>
<div className="App">
<Navbar />
<Route exact path="/" component={Landing} />
<Route exact path="/register" component={Register} />
<Route exact path="/login" component={Login} />
<Switch>
<PrivateRoute exact path="/dashboard" component={Dashboard} />
</Switch>
</div>
</Router>
</Provider>
);
}
}
export default App;
One last step!
It wouldn’t make sense for logged in users to be able to access the /login
and /register
pages. If a logged in user navigates to either of these, we should immediately redirect them to the dashboard.
To achieve this, add the following lifecycle method below the constructor in Register.js
.
componentDidMount() { // If logged in and user navigates to Register page, should redirect them to dashboard if (this.props.auth.isAuthenticated) { this.props.history.push("/dashboard"); } }
And add the same lifecycle method below the constructor in Login.js
.
componentDidMount() { // If logged in and user navigates to Login page, should redirect them to dashboard if (this.props.auth.isAuthenticated) { this.props.history.push("/dashboard"); } }
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK