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


Let’s implement a whole authentication mechanism in React & Redux 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-redux-typescript-firebase-auth

πŸ—½ Get Firebase App info

It will be needs for using Firebase authentication.

🚌 create-react-app with Redux and 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 redux react-redux recompose

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

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/components/Account
mkdir src/components/App
mkdir src/components/Home
mkdir src/components/Landing
mkdir src/components/Navigation
mkdir src/components/PasswordChange
mkdir src/components/PasswordForget
mkdir src/components/Session
mkdir src/components/SignIn
mkdir src/components/SignOut
mkdir src/components/SignUp
mkdir src/constants
mkdir src/firebase
mkdir src/reducers
mkdir src/store

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

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

πŸŽ‚ index

Create src/index.ts file:

import * as React from "react";
import * as ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { App } from "./components/App";
import "./index.css";
import registerServiceWorker from "./registerServiceWorker";
import { store } from "./store";

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
registerServiceWorker();

🐝 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

Account

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

import * as React from "react";
import { connect } from "react-redux";
import { compose } from "recompose";
import { withRouter } from 'react-router-dom'

import { PasswordChangeForm } from "../PasswordChange";
import { PasswordForgetForm } from "../PasswordForget/PasswordForgetForm";
import { withAuthorization } from "../Session/withAuthorization";

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

const mapStateToProps = (state: any) => ({
authUser: state.sessionState.authUser
});

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

export const Account = compose(
withAuthorization(authCondition),
withRouter,
connect(mapStateToProps)
)(AccountComponent);

App

Create src/components/App/index.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 { Account } from "../Account";
import { Home } from "../Home";
import { Landing } from "../Landing";
import { Navigation } from "../Navigation";
import { PasswordForget } from "../PasswordForget";
import { withAuthentication } from "../Session/withAuthentication";
import { SignIn } from "../SignIn";
import { SignUp } from "../SignUp";

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);

Home

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

import * as React from "react";
import { connect } from "react-redux";
import { compose } from "recompose";
import { withRouter } from 'react-router-dom'

import { db } from "../../firebase";
import { withAuthorization } from "../Session/withAuthorization";
import { UserList } from "./UserList";

class HomeComponent extends React.Component {
public componentDidMount() {
const { onSetUsers }: any = this.props;

db.onceGetUsers().then(snapshot => onSetUsers(snapshot.val()));
}

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

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

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

const mapStateToProps = (state: any) => ({
users: state.userState.users
});

const mapDispatchToProps = (dispatch: any) => ({
onSetUsers: (users: any) => dispatch({ type: "USERS_SET", users })
});

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

export const Home = compose(
withAuthorization(authCondition),
withRouter,
connect(
mapStateToProps,
mapDispatchToProps
)
)(HomeComponent);

Create src/components/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 Usernames of Users</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/components/Landing/index.tsx file:

import * as React from "react";

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

Navigation

Create src/components/Navigation/index.tsx file:

import * as React from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";

import * as routes from "../../constants/routes";
import { SignOutButton } from "../SignOut";

const NavigationComponent = ({ authUser }: any) => (
<div>{authUser ? <NavigationAuth /> : <NavigationNonAuth />}</div>
);

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>
);

const mapStateToProps = (state: any) => ({
authUser: state.sessionState.authUser
});

export const Home = compose(
withRouter,
connect(mapStateToProps)
)(NavigationComponent);

PasswordChange

Create src/components/PasswordChange/index.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)
);
}
}

PasswordForget

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

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

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

export const PasswordForgetLink = () => (
<p>
<Link to={routes.PASSWORD_FORGET}>Forgot Password</Link>
</p>
);

Create src/components/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)
);
}
}

πŸ‘½ Session

Create src/components/Session/withAuthentication.tsx file:

import * as React from "react";
import { connect } from "react-redux";
import { firebase } from "../../firebase";

interface InterfaceProps {
authUser?: any;
}

interface InterfaceState {
authUser?: any;
}

export const withAuthentication = (Component: any) => {
class WithAuthentication extends React.Component<
InterfaceProps,
InterfaceState
> {
public componentDidMount() {
const { onSetAuthUser }: any = this.props;

firebase.auth.onAuthStateChanged(authUser => {
authUser ? onSetAuthUser(authUser) : onSetAuthUser(null);
});
}

public render() {
return <Component />;
}
}

const mapDispatchToProps = (dispatch: any) => ({
onSetAuthUser: (authUser: any) =>
dispatch({ type: "AUTH_USER_SET", authUser })
});

return connect(
null,
mapDispatchToProps
)(WithAuthentication);
};

Create src/components/Session/withAuthorization.tsx file:

import * as React from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import { compose } from "recompose";
import * as routes from "../../constants/routes";
import { firebase } from "../../firebase";

interface InterfaceProps {
history?: any;
authUser?: any;
}

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

public render() {
return this.props.authUser ? <Component /> : null;
}
}

const mapStateToProps = (state: any) => ({
authUser: state.sessionState.authUser
});

return compose(
withRouter,
connect(mapStateToProps)
)(WithAuthorization);
};

SignIn

Create src/components/SignIn/index.tsx:

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/components/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));
}
}

SignOut

Create src/components/SignOut/index.tsx file:

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

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

SignUp

Create src/components/SignUp/index.tsx file:

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

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

Create SingUpForm.tsx file:

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

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

interface InterfaceState {
email: string;
error: any;
passwordOne: string;
passwordTwo: string;
username: string;
}

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

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

constructor(props: InterfaceProps) {
super(props);
this.state = { ...SignUpForm.INITIAL_STATE };
}

public onSubmit(event: any) {
event.preventDefault();

const { email, passwordOne, username } = this.state;
const { history } = this.props;

auth
.doCreateUserWithEmailAndPassword(email, passwordOne)
.then((authUser: any) => {

// Create a user in your own accessible Firebase Database too
db.doCreateUser(authUser.user.uid, username, email)
.then(() => {

this.setState(() => ({ ...SignUpForm.INITIAL_STATE }));
history.push(routes.HOME);
})
.catch(error => {
this.setState(SignUpForm.propKey("error", error));
});
})
.catch(error => {
this.setState(SignUpForm.propKey("error", error));
});
}

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

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

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

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

private setStateWithEvent(event: any, columnType: string) {
this.setState(SignUpForm.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.

πŸš• 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!");
};

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 };

😸 reducers

index

Create src/reducers/index.ts file:

import { combineReducers } from "redux";
import { sessionReducer } from "./session";
import { userReducer } from "./user";

export const rootReducer = combineReducers({
sessionState: sessionReducer,
userState: userReducer
});

session

Create src/reducers/session.ts file:

const INITIAL_STATE = {
authUser: null
};

const applySetAuthUser = (state: any, action: any) => ({
...state,
authUser: action.authUser
});

export function sessionReducer(state = INITIAL_STATE, action: any) {
switch (action.type) {
case "AUTH_USER_SET": {
return applySetAuthUser(state, action);
}
default:
return state;
}
}

user

Create src/reducers/user.ts file:

const INITIAL_STATE = {
users: {}
};

const applySetUsers = (state: any, action: any) => ({
...state,
users: action.users
});

export function userReducer(state = INITIAL_STATE, action: any) {
switch (action.type) {
case "USERS_SET": {
return applySetUsers(state, action);
}
default:
return state;
}
}

πŸ—» store

Create src/store/index.ts file:

import { createStore } from "redux";
import { rootReducer } from "../reducers";

const store = createStore(rootReducer);

export default store;

🚜 GitHub Repository

Whole files of this tutorial are as follows:

https://github.com/morizyun/react-redux-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!!