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:
yarn global add create-react-app create-react-app react-ts-firebase-auth --scripts-version=react-scripts-ts cd react-ts-firebase-authyarn 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 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 mv src/components/App.ts src/components/App/index.ts 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>, 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 ) => (
Account: {authUser.email}</h1> > </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 (
true } path={routes.LANDING} component={Landing} /> true } path={routes.SIGN_UP} component={SignUp} /> true } path={routes.SIGN_IN} component={SignIn} /> exact={true } path={routes.PASSWORD_FORGET} component={PasswordForget} /> true } path={routes.HOME} component={Home} /> 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 (
Home</h1> The Home Page is accessible by every signed in user.p>
{!!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 { constructor (props: any ) { super (props); } public render() { const { users }: any = this .props; return (
List of Usernames of Users</h2> (Saved on Sign Up in Firebase Database)p>
{Object .keys(users).map(key => { return {users[key].username}</li>; })} l> </div> ); } }
Landing Create src/components/Landing/index.tsx
file:
import * as React from "react" ;export const Landing = () => { return (
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 ) => ( {authUser ? : }</div>
); const NavigationAuth = () => ( LandingLink> </li> HomeLink> </li> AccountLink> </li> > </li> l> ); const NavigationNonAuth = () => ( Landing</Link> li> 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 ( this .onSubmit(event)}> value={passwordOne} onChange={event => this .setStateWithEvent(event, "passwordOne" )} type ="password" placeholder="New Password" /> value={passwordTwo} onChange={event => this .setStateWithEvent(event, "passwordTwo" )} type ="password" placeholder="Confirm New Password" /> type ="submit" > Reset My Password </button> {error && {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 = () => (
PasswordForget</h1> > </div> ); export const PasswordForgetLink = () => (
Forgot PasswordLink> </p> );
Create src/components/PasswordForget/PasswordForgetForm.tsx
file:
🏈 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 ; } } 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.Componentany > { public componentDidMount() { firebase.auth.onAuthStateChanged(authUser => { if (!condition(authUser)) { this .props.history.push(routes.SIGN_IN); } }); } public render() { return this .props.authUser ? : 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 } ) => (
SignIn</h1> > </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 ( this .onSubmit(event)}> value={email} onChange={event => this .setStateWithEvent(event, "email" )} type ="text" placeholder="Email Address" /> value={password} onChange={event => this .setStateWithEvent(event, "password" )} type ="password" placeholder="Password" /> type ="submit" > Sign In </button> {error && {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 = () => ( 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 = () => ( 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 ) => { 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 ( this .onSubmit(event)}> value={username} onChange={event => this .setStateWithEvent(event, "username" )} type ="text" placeholder="Full Name" /> value={email} onChange={event => this .setStateWithEvent(event, "email" )} type ="text" placeholder="Email Address" /> value={passwordOne} onChange={event => this .setStateWithEvent(event, "passwordOne" )} type ="password" placeholder="Password" /> value={passwordTwo} onChange={event => this .setStateWithEvent(event, "passwordTwo" )} type ="password" placeholder="Confirm Password" /> type ="submit" > Sign Up </button> {error && {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" ;export const doCreateUserWithEmailAndPassword = ( email: string , password: string ) => auth.createUserWithEmailAndPassword(email, password); export const doSignInWithEmailAndPassword = (email: string , password: string ) => auth.signInWithEmailAndPassword(email, password); export const doSignOut = () => auth.signOut();export const doPasswordReset = (email: string ) => auth.sendPasswordResetEmail(email); 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" ;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 !!