We're going to build a simple authentication and secure application in React and Firebase Authentication SDKs. Users will have the ability to create account, sign in and sign out. We'll also make certain routes (private pages) secure and protected to be only used by authenticated users. I hope you find it helpful!
Application Installation & Setup
To get started we'll create an application that will be bootstrapped with Facebook’s official React boilerplate create-react-app.
Run
npm i -g create-react-app
to have it installed on your local machine
# Creating an App
create-react-app react-firebase-auth
# Change directory
cd react-firebase-auth
# Additional packages to install
yarn add firebase react-router-dom react-props
The initial project structure is now generated, and all dependencies installed successfully. Let's rock up our project hierarchy and it's folder structure as showing below:
# Make sub-directories under src/ path
mkdir -p src/{components,firebase,shared}
# Move App component under src/components
mv src/App.js src/components
# Create desired files to work with
touch src/components/{AppProvider,Navbar,FlashMessage,Login,Signup}.js
touch src/firebase/{firebase,index,auth,config}.js
touch src/shared/Form.js
We need to make sure that everything fall into place by listing all created files, and sub-directories via command line cd src/ && ls * -r
# terminal
react-firebase-auth % cd src/ && ls * -r
components:
App.js AppProvider.js FlashMessage.js Login.js Navbar.js Signup.js
firebase:
auth.js config.js firebase.js index.js
shared:
Form.js
Firebase
We're not going to deep dive into Firebase itself.
If you're not familiar with Firebase please make sure you check their guide
on how to Add Firebase to your JavaScript Project
Firebase configuration
// src/firebase/config.js
const devConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "AUTH_DOMAIN",
databaseURL: "DATABASE_URL",
projectId: "PROJECT_ID",
storageBucket: "STORAGE_BUCKET",
messagingSenderId: "MESSAGING_SENDER_ID"
};
const prodConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "AUTH_DOMAIN",
databaseURL: "DATABASE_URL",
projectId: "PROJECT_ID",
storageBucket: "STORAGE_BUCKET",
messagingSenderId: "MESSAGING_SENDER_ID"
};
export {
devConfig,
prodConfig
}
Config breakdown
- devConfig used for development environment
- prodConfig used for production environment
📌 its alway good to have a config template file for your project with predefined setup (as showing above) to avoid pushing sensitive data to a repository. You or any one of your team can later make a copy of this template with the proper file extension. Example (based on this post): Create a file firebase.config
open your .gitignore
and add app/config.js
then run cp app/firebase.config app/config.js
to copy of that config template.
Firebase initialization
// src/firebase/firebase.js
import * as firebase from 'firebase';
import { devConfig } from './config';
!firebase.apps.length && firebase.initializeApp(devConfig);
const auth = firebase.auth();
export {
auth
}
Auth module
// src/firebase/auth.js
import { auth } from './firebase';
/**
* Create user session
* @param {string} action - createUser, signIn
* @param {string} email
* @param {string} password
*/
const userSession = (action, email, password) => auth[`${action}WithEmailAndPassword`](email, password);
/**
* Destroy current user session
*/
const logout = () => auth.signOut();
export {
userSession,
logout
}
Auth module breakdown
- userSession a function that accepts three params action: decides whether user creates an account or login, email and password
- logout destroys the current user session and log the user out of the system
Firebase module
// src/firebase/index.js
import * as auth from './auth';
import * as firebase from './firebase';
export {
auth,
firebase
}
Components
Provider component
// src/components/AppProvider.js
import React, {
Component,
createContext
} from 'react';
import { firebase } from '../firebase';
export const {
Provider,
Consumer
} = createContext();
class AppProvider extends Component {
state = {
currentUser: AppProvider.defaultProps.currentUser,
message: AppProvider.defaultProps.message
}
componentDidMount() {
firebase.auth.onAuthStateChanged(user => user && this.setState({
currentUser: user
}))
}
render() {
return (
<Provider value={{
state: this.state,
destroySession: () => this.setState({
currentUser: AppProvider.defaultProps.currentUser
}),
setMessage: message => this.setState({ message }),
clearMessage: () => this.setState({
message: AppProvider.defaultProps.message
})
}}>
{this.props.children}
</Provider>
)
}
}
AppProvider.defaultProps = {
currentUser: null,
message: null
}
export default AppProvider;
AppProvider breakdown
AppProvider is a React component provides a way to pass data through the component tree without having to pass props down manually at every level and allows Consumers to subscribe to context changes.
- componentDidMount after a component is mounted, we check against user existence.
Navbar component
// src/components/Navbar.js
import React from 'react';
import {
Link,
withRouter
} from 'react-router-dom';
import { auth } from '../firebase';
import { Consumer } from './AppProvider';
const Navbar = props => {
const handleLogout = context => {
auth.logout();
context.destroySession();
props.history.push('/signedOut');
};
return <Consumer>
{({ state, ...context }) => (
state.currentUser ?
<ul>
<li><Link to="/dashboard">Dashboard</Link></li>
<li><a onClick={() => handleLogout(context)}>Logout</a></li>
</ul>
:
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/login">Login</Link></li>
<li><Link to="/signup">Create Account</Link></li>
</ul>
)}
</Consumer>
};
export default withRouter(Navbar);
Navbar breakdown
The Navbar component handles UI logic as the following:
- If the system logged in user then we show Dashboard (protected page) and the Logout button which kicks out the user and redirect to
/signedOut
page. - If no users found then we display Home, Login and Create and Account links.
FlashMessage component
// src/components/FlashMessage.js
import React from 'react';
import { Consumer } from '../components/AppProvider';
const FlashMessage = () => <Consumer>
{({ state, ...context }) => state.message && <small className="flash-message">
{state.message}
<button type="button" onClick={() => context.clearMessage()}>Ok</button>
</small>}
</Consumer>;
export default FlashMessage;
FlashMessage breakdown
FlashMessage is a stateless component wrapped by Consumer that subscribes to context changes. It shows up when something goes wrong (i.e. Form validation, server error, etc...). The FlashMessage has "Ok" button that clears it up and close/hide it.
Form component
// src/shared/Form.js
import React, {
Component,
createRef
} from 'react';
import PropTypes from 'prop-types';
import { auth } from '../firebase';
class Form extends Component {
constructor(props) {
super(props);
this.email = createRef();
this.password = createRef();
this.handleSuccess = this.handleSuccess.bind(this);
this.handleErrors = this.handleErrors.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleSuccess() {
this.resetForm();
this.props.onSuccess && this.props.onSuccess();
}
handleErrors(reason) {
this.props.onError && this.props.onError(reason);
}
handleSubmit(event) {
event.preventDefault();
const {
email,
password,
props: { action }
} = this;
auth.userSession(
action,
email.current.value,
password.current.value
).then(this.handleSuccess).catch(this.handleErrors);
}
resetForm() {
if (!this.email.current || !this.password.current) { return }
const { email, password } = Form.defaultProps;
this.email.current.value = email;
this.password.current.value = password;
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<h1>{this.props.title}</h1>
<input
name="name"
type="email"
ref={this.email}
/>
<input
name="password"
type="password"
autoComplete="none"
ref={this.password}
/>
<button type="submit">Submit</button>
</form>
)
}
}
Form.propTypes = {
title: PropTypes.string.isRequired,
action: PropTypes.string.isRequired,
onSuccess: PropTypes.func,
onError: PropTypes.func
}
Form.defaultProps = {
errors: '',
email: '',
password: ''
}
export default Form;
Form breakdown
- Both email, password creates a ref
createRef()
that we attach later to React elements via the ref attribute. -
handleSuccess method executes
resetForm
method, and callback function from the giving props (if found any!). - handleErrors method executes the callback function from the giving props (if found any!) with reason.
-
handleSubmit method prevent the default
form
behavior, and executes theauth.userSession
to create and account or login a user.
Login component
// src/components/Login.js
import React from 'react';
import { withRouter } from 'react-router-dom';
import Form from '../shared/Form';
import { Consumer } from './AppProvider';
const Login = props => <Consumer>
{({ state, ...context }) => (
<Form
action="signIn"
title="Login"
onSuccess={() => props.history.push('/dashboard')}
onError={({ message }) => context.setMessage(`Login failed: ${message}`)}
/>
)}
</Consumer>;
export default withRouter(Login);
Login breakdown
Login is a stateless component wrapped by Consumer that subscribes to context changes. If successfully logged in the user will be redirect to a protected page (dashboard) otherwise error message will be popped up.
Signup component
// src/components/Signup.js
import React from 'react';
import { withRouter } from 'react-router-dom';
import Form from '../shared/Form';
import { auth } from '../firebase';
import { Consumer } from './AppProvider';
const Signup = props => <Consumer>
{({ state, ...context }) => (
<Form
action="createUser"
title="Create account"
onSuccess={() => auth.logout().then(() => {
context.destroySession();
context.clearMessage();
props.history.push('/accountCreated');
})}
onError={({ message }) => context.setMessage(`Error occured: ${message}`)}
/>
)}
</Consumer>;
export default withRouter(Signup);
Signup breakdown
Signup is a stateless component wrapped by Consumer that subscribes to context changes. Firebase by default automatically logs the user in once account created successfully. I've changed this implementation by making the user log in manually after account creation. Once onSuccess
callback fires we log the user out, and redirect to /accountCreated
page with custom message and a call to action "Proceed to Dashboard" link to login. If account creation fails error message will be popped up.
App component (container)
// src/components/App.js
import React, {
Component,
Fragment
} from 'react';
import {
BrowserRouter as Router,
Route,
Link
} from 'react-router-dom';
import AppProvider, {
Consumer
} from './AppProvider';
import Login from './Login';
import Signup from './Signup';
import Navbar from '../shared/Navbar';
import FlashMessage from '../shared/FlashMessage';
class App extends Component {
render() {
return (
<AppProvider>
<Router>
<Fragment>
<Navbar />
<FlashMessage />
<Route exact path="/" component={() =>
<h1 className="content">Welcome, Home!</h1>} />
<Route exact path="/login" component={() => <Login />} />
<Route exact path="/signup" component={() => <Signup />} />
<Router exact path="/dashboard" component={() => <Consumer>
{
({ state }) => state.currentUser ?
<h1 className="content">Protected dashboard!</h1> :
<div className="content">
<h1>Access denied.</h1>
<p>You are not authorized to access this page.</p>
</div>
}
</Consumer>} />
<Route exact path="/signedOut" component={() =>
<h1 className="content">You're now signed out.</h1>} />
<Route exact path="/accountCreated" component={() =>
<h1 className="content">Account created. <Link to="/login">
Proceed to Dashboard</Link></h1>} />
</Fragment>
</Router>
</AppProvider>
);
}
}
export default App;
App (container) breakdown
Its pretty straightforward right here! The navigational components Routers wrapped by AppProvider to pass data through the component tree. The /dashboard
route component has a protected content (page) that is served only for authenticated users, and no users are signed in we display the Access denied message instead of our private content/page.
Demo
Check out demo-gif here
Feedback are welcome If you have any suggestions or corrections to make, please do not hesitate to drop me a note/comment.
Top comments (3)
I would get rid of
auth.js
. Seems like extra indirection for not much gain.You can refactor
action
in the form:Then in
Form.js
I recently made something similar using React.Context to manage auth state. On some advice from @kentcdodds he made a point how, if your entire app depends on an authorized user, it's much easier to handle it all in it's own module then just import user state in the files that need it, in the Components that are only rendered for an authorized user.
With firebase auth I'm just rendering a complete different root Component (App or Login) in my main index file like this:
But this could also be
renderApp(user)
and you could define routes appropriately in there. Easier to not have to check for user everywhere, and in each Component you can just:where needed, knowing the user is logged in at this point.
A couple of typos I found. Might be useful for other people who are working through the tutorial!
1)
In src/components/App.js the folder specified for Navbar and FlashMessage is "shared/" instead of "components/". In the first step these files are added to the "components/" folder. So src/components/App.js should have the following 2 lines changed.
import Navbar from '../components/Navbar';
import FlashMessage from '../components/FlashMessage';
2)
Since we have our App.js under "components/" we will need to change the default src/index.js that you get from create-react-app. The default index.js assumes App.js is located under the src folder and the path needs to be udpated to components/App.js. This was not mentioned in the tutorial.
I am sort of confused by this however it does work. I am trying to understand how to add another component that would act as a better dashboard.