create-react-app with TypeScript & Firebase Authentication [JavaScript]


Let’s implement a whole authentication mechanism in React with sign up, sign in, password reset, change password and sign out in TypeScript.

πŸ€” GitHub Repository

Whole files of this tutorial are as follows:

https://github.com/morizyun/react-typescript-firebase-auth

😼 Get Firebase App info

It will be needs for using Firebase authentication.

πŸ—½ create-react-app with TypeScript

You can install create-react-app on command line:

# If you have not install it yet, please do:
yarn global add create-react-app # npm install -g create-react-app

# Create new project
create-react-app react-ts-firebase-auth --scripts-version=react-scripts-ts
cd react-ts-firebase-auth

# Install Firebase and react-router-dom (React Router)
yarn add firebase react-router-dom

yarn add --dev @types/react-router @types/react-router-dom

After then, you can run app process by yarn start on terminal and open http://localhost:3000.

Also, please create some directories:

😸 Configuration

Please change tslint.json to add as follows:

{
+ "rules": {
+ "jsx-no-lambda": false
+ }
}

πŸ€ Rebuild folders

# Create folders
mkdir src/components
mkdir src/constants
mkdir src/firebase
mkdir -p src/pages/Account
mkdir src/pages/Admin
mkdir src/pages/Home
mkdir src/pages/Landing
mkdir src/pages/PasswordForget
mkdir src/pages/SignIn
mkdir src/pages/SignUp

# Move files for App
mv src/components/App.ts src/components/App.ts

# Remove unused files
rm src/logo.svg
rm src/App.test.ts
rm src/App.css

πŸš• Constants

Add src/constants/routes.ts file:

export const SIGN_UP = "/signup";
export const SIGN_IN = "/signin";
export const LANDING = "/";
export const HOME = "/home";
export const ACCOUNT = "/account";
export const PASSWORD_FORGET = "/password_forget";

🐞 Components

App

Create src/components/App.tsx file:

import * as React from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import * as routes from "../constants/routes";
import { firebase } from "../firebase";
import { withAuthentication } from "../firebase/withAuthentication";
import { Account } from "../pages/Account";
import { Home } from "../pages/Home";
import { Landing } from "../pages/Landing";
import { PasswordForget } from "../pages/PasswordForget";
import { SignIn } from "../pages/SignIn";
import { SignUp } from "../pages/SignUp";
import { Navigation } from "./Navigation";

class AppComponent extends React.Component {
constructor(props: any) {
super(props);

this.state = {
authUser: null
};
}

public componentDidMount() {
firebase.auth.onAuthStateChanged(authUser => {
authUser
? this.setState(() => ({ authUser }))
: this.setState(() => ({ authUser: null }));
});
}

public render() {
return (
<BrowserRouter>
<div>
<Navigation />
<hr />
<Switch>
<Route exact={true} path={routes.LANDING} component={Landing} />
<Route exact={true} path={routes.SIGN_UP} component={SignUp} />
<Route exact={true} path={routes.SIGN_IN} component={SignIn} />
<Route
exact={true}
path={routes.PASSWORD_FORGET}
component={PasswordForget}
/>
<Route exact={true} path={routes.HOME} component={Home} />
<Route exact={true} path={routes.ACCOUNT} component={Account} />
</Switch>
</div>
</BrowserRouter>
);
}
}

export const App = withAuthentication(AppComponent);

Navigation

Create src/components/Navigation.tsx file:

import * as React from "react";
import { Link } from "react-router-dom";
import * as routes from "../constants/routes";
import { AuthUserContext } from "../firebase/AuthUserContext";
import { SignOutButton } from "./SignOutButton";

export const Navigation = () => (
<AuthUserContext.Consumer>
{authUser => (authUser ? <NavigationAuth /> : <NavigationNonAuth />)}
</AuthUserContext.Consumer>
);

const NavigationAuth = () => (
<ul>
<li>
<Link to={routes.LANDING}>Landing</Link>
</li>
<li>
<Link to={routes.HOME}>Home</Link>
</li>
<li>
<Link to={routes.ACCOUNT}>Account</Link>
</li>
<li>
<SignOutButton />
</li>
</ul>
);

const NavigationNonAuth = () => (
<ul>
<li>
<Link to={routes.LANDING}>Landing</Link>
</li>
<li>
<Link to={routes.SIGN_IN}>Sign In</Link>
</li>
</ul>
);

PasswordChangeForm

Create src/components/PasswordChangeForm.tsx file:

import * as React from "react";
import { auth } from "../firebase";

interface InterfaceProps {
error?: any;
history?: any;
passwordOne?: string;
passwordTwo?: string;
}

interface InterfaceState {
error?: any;
passwordOne?: string;
passwordTwo?: string;
}

export class PasswordChangeForm extends React.Component<
InterfaceProps,
InterfaceState
> {
private static INITIAL_STATE = {
error: null,
passwordOne: "",
passwordTwo: ""
};

private static propKey(propertyName: string, value: string): object {
return { [propertyName]: value };
}

constructor(props: any) {
super(props);
this.state = { ...PasswordChangeForm.INITIAL_STATE };
}

public onSubmit = (event: any) => {
const { passwordOne }: any = this.state;

auth
.doPasswordUpdate(passwordOne)
.then(() => {
this.setState(() => ({ ...PasswordChangeForm.INITIAL_STATE }));
})
.catch(error => {
this.setState(PasswordChangeForm.propKey("error", error));
});

event.preventDefault();
};

public render() {
const { passwordOne, passwordTwo, error }: any = this.state;

const isInvalid = passwordOne !== passwordTwo || passwordOne === "";

return (
<form onSubmit={event => this.onSubmit(event)}>
<input
value={passwordOne}
onChange={event => this.setStateWithEvent(event, "passwordOne")}
type="password"
placeholder="New Password"
/>
<input
value={passwordTwo}
onChange={event => this.setStateWithEvent(event, "passwordTwo")}
type="password"
placeholder="Confirm New Password"
/>
<button disabled={isInvalid} type="submit">
Reset My Password
</button>

{error && <p>{error.message}</p>}
</form>
);
}

private setStateWithEvent(event: any, columnType: string): void {
this.setState(
PasswordChangeForm.propKey(columnType, (event.target as any).value)
);
}
}

SignOutButton

Create src/components/SignOutButton.tsx file:

import * as React from "react";
import { auth } from "../firebase";

export const SignOutButton = () => (
<button type="button" onClick={auth.doSignOut}>
Sign Out
</button>
);

🚌 firebase

auth

Create src/firebase/auth.tsx file:

import { auth } from "./firebase";

// Sign Up
export const doCreateUserWithEmailAndPassword = (
email: string,
password: string
) => auth.createUserWithEmailAndPassword(email, password);

// Sign In
export const doSignInWithEmailAndPassword = (email: string, password: string) =>
auth.signInWithEmailAndPassword(email, password);

// Sign out
export const doSignOut = () => auth.signOut();

// Password Reset
export const doPasswordReset = (email: string) =>
auth.sendPasswordResetEmail(email);

// Password Change
export const doPasswordUpdate = async (password: string) => {
if (auth.currentUser) {
await auth.currentUser.updatePassword(password);
}
throw Error("No auth.currentUser!");
};

AuthUserContext

Create src/firebase/AuthUserContext.ts file:

import * as React from "react";

export const AuthUserContext = React.createContext(null);

db

Create src/firebase/db.ts file:

import { db } from "./firebase";

// User API
export const doCreateUser = (id: string, username: string, email: string) =>
db.ref(`users/${id}`).set({
email,
username
});

export const onceGetUsers = () => db.ref("users").once("value");

firebase

Create src/firebase/firebase.ts file:

import * as firebase from "firebase/app";
import "firebase/auth";
import "firebase/database";

const config = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
databaseURL: "YOUR_DATABASE_URL",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
projectId: "YOUR PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET"
};

if (!firebase.apps.length) {
firebase.initializeApp(config);
}

export const auth = firebase.auth();
export const db = firebase.database();

index

Create src/firebase/index.ts file:

import * as auth from "./auth";
import * as db from "./db";
import * as firebase from "./firebase";

export { auth, db, firebase };

withAuthentication

Create src/firebase/withAuthentication.tsx file:

import * as React from "react";
import { firebase } from "../firebase";
import { AuthUserContext } from "./AuthUserContext";

interface InterfaceProps {
authUser?: any;
}

interface InterfaceState {
authUser?: any;
}

export const withAuthentication = (Component: any) => {
class WithAuthentication extends React.Component<
InterfaceProps,
InterfaceState
> {
constructor(props: any) {
super(props);

this.state = {
authUser: null
};
}

public componentDidMount() {
firebase.auth.onAuthStateChanged(authUser => {
authUser
? this.setState(() => ({ authUser }))
: this.setState(() => ({ authUser: null }));
});
}

public render() {
const { authUser } = this.state;

return (
<AuthUserContext.Provider value={authUser}>
<Component />
</AuthUserContext.Provider>
);
}
}
return WithAuthentication;
};

withAuthorization

Create src/firebase/withAuthorization.tsx fle:

import * as React from "react";
import { withRouter } from "react-router-dom";
import * as routes from "../constants/routes";
import { firebase } from "../firebase";
import { AuthUserContext } from "./AuthUserContext";

interface InterfaceProps {
history?: any;
}

export const withAuthorization = (condition: any) => (Component: any) => {
class WithAuthorization extends React.Component<InterfaceProps, {}> {
public componentDidMount() {
firebase.auth.onAuthStateChanged(authUser => {
if (!condition(authUser)) {
this.props.history.push(routes.SIGN_IN);
}
});
}

public render() {
return (
<AuthUserContext.Consumer>
{authUser => (authUser ? <Component /> : null)}
</AuthUserContext.Consumer>
);
}
}

return withRouter(WithAuthorization as any);
};

🍣 Pages

Account

Create src/pages/Account/index.tsx file:

import * as React from "react";
import { PasswordChangeForm } from "./PasswordChangeForm";
import { AuthUserContext } from "../../firebase/AuthUserContext";
import { withAuthorization } from "../../firebase/withAuthorization";
import { PasswordForgetForm } from "../PasswordForget/PasswordForgetForm";

export const AccountComponent = () => (
<AuthUserContext.Consumer>
{authUser => (
<div>
<h1>Account: {(authUser as any).email}</h1>
<PasswordForgetForm />
<PasswordChangeForm />
</div>
)}
</AuthUserContext.Consumer>
);

const authCondition = (authUser: any) => !!authUser;

export const Account = withAuthorization(authCondition)(AccountComponent);

Create src/pages/Account/PasswordChangeForm.tsx file:

import * as React from "react";
import { auth } from "../../firebase";

interface InterfaceProps {
error?: any;
history?: any;
passwordOne?: string;
passwordTwo?: string;
}

interface InterfaceState {
error?: any;
passwordOne?: string;
passwordTwo?: string;
}

export class PasswordChangeForm extends React.Component<
InterfaceProps,
InterfaceState
> {
private static INITIAL_STATE = {
error: null,
passwordOne: "",
passwordTwo: ""
};

private static propKey(propertyName: string, value: string): object {
return { [propertyName]: value };
}

constructor(props: any) {
super(props);
this.state = { ...PasswordChangeForm.INITIAL_STATE };
}

public onSubmit = (event: any) => {
const { passwordOne }: any = this.state;

auth
.doPasswordUpdate(passwordOne)
.then(() => {
this.setState(() => ({ ...PasswordChangeForm.INITIAL_STATE }));
})
.catch(error => {
this.setState(PasswordChangeForm.propKey("error", error));
});

event.preventDefault();
};

public render() {
const { passwordOne, passwordTwo, error }: any = this.state;

const isInvalid = passwordOne !== passwordTwo || passwordOne === "";

return (
<form onSubmit={event => this.onSubmit(event)}>
<input
value={passwordOne}
onChange={event => this.setStateWithEvent(event, "passwordOne")}
type="password"
placeholder="New Password"
/>
<input
value={passwordTwo}
onChange={event => this.setStateWithEvent(event, "passwordTwo")}
type="password"
placeholder="Confirm New Password"
/>
<button disabled={isInvalid} type="submit">
Reset My Password
</button>

{error && <p>{error.message}</p>}
</form>
);
}

private setStateWithEvent(event: any, columnType: string): void {
this.setState(
PasswordChangeForm.propKey(columnType, (event.target as any).value)
);
}
}

Home

Create src/pages/Home/index.tsx file:

import * as React from "react";
import { db } from "../../firebase";
import { withAuthorization } from "../../firebase/withAuthorization";
import { UserList } from "./UserList";

class HomeComponent extends React.Component {
constructor(props: any) {
super(props);

this.state = {
users: null
};
}

public componentDidMount() {
db.onceGetUsers().then(snapshot =>
this.setState(() => ({ users: snapshot.val() }))
);
}

public render() {
const { users }: any = this.state;

return (
<div>
<h2>Home Page</h2>
<p>The Home Page is accessible by every signed in user.</p>

{!!users && <UserList users={users} />}
</div>
);
}
}

const authCondition = (authUser: any) => !!authUser;

export const Home = withAuthorization(authCondition)(HomeComponent);

Create src/pages/Home/UserList.tsx file:

import * as React from "react";

interface InterfaceProps {
users?: any;
}

export class UserList extends React.Component<InterfaceProps, {}> {
constructor(props: any) {
super(props);
}

public render() {
const { users }: any = this.props;

return (
<div>
<h2>List of User name</h2>
<p>(Saved on Sign Up in Firebase Database)</p>

<ul>
{Object.keys(users).map(key => {
return <li key={key}>{users[key].username}</li>;
})}
</ul>
</div>
);
}
}

Landing

Create src/pages/Landing/index.tsx file:

import * as React from "react";

export const Landing = () => {
return (
<div>
<h2>Landing Page</h2>
</div>
);
};

PasswordForget

Create src/pages/PasswordForget/index.tsx file:

import * as React from "react";
import { Link } from "react-router-dom";
import { PasswordForgetForm } from "./PasswordForgetForm";

export const PasswordForget = () => (
<div>
<h1>PasswordForget</h1>
<PasswordForgetForm />
</div>
);

export const PasswordForgetLink = () => (
<p>
<Link to="/pw-forget">Forgot Password</Link>
</p>
);

Create src/pages/PasswordForget/PasswordForgetForm.tsx file:

import * as React from "react";
import { auth } from "../../firebase";

export class PasswordForgetForm extends React.Component {
private static INITIAL_STATE = {
email: "",
error: null
};

private static propKey(propertyName: string, value: string) {
return { [propertyName]: value };
}

constructor(props: any) {
super(props);

this.state = { ...PasswordForgetForm.INITIAL_STATE };
}

public onSubmit = (event: any) => {
const { email }: any = this.state;

auth
.doPasswordReset(email)
.then(() => {
this.setState(() => ({ ...PasswordForgetForm.INITIAL_STATE }));
})
.catch(error => {
this.setState(PasswordForgetForm.propKey("error", error));
});

event.preventDefault();
};

public render() {
const { email, error }: any = this.state;
const isInvalid = email === "";

return (
<form onSubmit={(event) => this.onSubmit(event)}>
<input
value={email}
onChange={(event) => this.setStateWithEvent(event, "email")}
type="text"
placeholder="Email Address"
/>
<button disabled={isInvalid} type="submit">
Reset My Password
</button>

{error && <p>{error.message}</p>}
</form>
);
}

private setStateWithEvent(event: any, columnType: string): void {
this.setState(
PasswordForgetForm.propKey(columnType, (event.target as any).value)
);
}
}

SignIn

Create src/pages/SignIn/index.tsx file:

import * as React from "react";
import { withRouter } from "react-router-dom";
import { PasswordForgetLink } from "../PasswordForget";
import { SignUpLink } from "../SignUp";
import { SignInForm } from "./SignInForm";

const SignInComponent = ({ history }: { [key: string]: any }) => (
<div>
<h1>SignIn</h1>
<SignInForm history={history} />
<SignUpLink />
<PasswordForgetLink />
</div>
);

export const SignIn = withRouter(SignInComponent);

Create src/pages/SignIn/SignInForm.tsx file:

import * as React from "react";
import * as routes from "../../constants/routes";
import { auth } from "../../firebase";

interface InterfaceProps {
email?: string;
error?: any;
history?: any;
password?: string;
}

interface InterfaceState {
email: string;
error: any;
password: string;
}

export class SignInForm extends React.Component<
InterfaceProps,
InterfaceState
> {
private static INITIAL_STATE = {
email: "",
error: null,
password: ""
};

private static propKey(propertyName: string, value: any): object {
return { [propertyName]: value };
}

constructor(props: InterfaceProps) {
super(props);

this.state = { ...SignInForm.INITIAL_STATE };
}

public onSubmit = (event: any) => {
const { email, password } = this.state;

const { history } = this.props;

auth
.doSignInWithEmailAndPassword(email, password)
.then(() => {
this.setState(() => ({ ...SignInForm.INITIAL_STATE }));
history.push(routes.HOME);
})
.catch(error => {
this.setState(SignInForm.propKey("error", error));
});

event.preventDefault();
};

public render() {
const { email, password, error } = this.state;

const isInvalid = password === "" || email === "";

return (
<form onSubmit={event => this.onSubmit(event)}>
<input
value={email}
onChange={event => this.setStateWithEvent(event, "email")}
type="text"
placeholder="Email Address"
/>
<input
value={password}
onChange={event => this.setStateWithEvent(event, "password")}
type="password"
placeholder="Password"
/>
<button disabled={isInvalid} type="submit">
Sign In
</button>

{error && <p>{error.message}</p>}
</form>
);
}

private setStateWithEvent(event: any, columnType: string): void {
this.setState(SignInForm.propKey(columnType, (event.target as any).value));
}
}

After then, please confirm http://localhost:3000 again. You can see some pages for sign up, sign in, password reset, change password and sign out.

πŸ‘½ GitHub Repository

Whole files of this tutorial are as follows:

https://github.com/morizyun/react-typescript-firebase-auth

πŸ˜€ References

πŸ–₯ Recommended VPS Service

VULTR provides high performance cloud compute environment for you. Vultr has 15 data-centers strategically placed around the globe, you can use a VPS with 512 MB memory for just $ 2.5 / month ($ 0.004 / hour). In addition, Vultr is up to 4 times faster than the competition, so please check it => Check Benchmark Results!!