Prerequisite

  • Bisa membaca kode JavaScript
  • Sedikit tahu tentang TypeScript, (Type definition)
  • Cukup mengerti ReactJS

Yup, untuk mengikuti code walk-through ini, setidaknya teman-teman sudah bisa membaca kode JavaScript atau tahu tentang TypeScript dan sedikit tahu tentang ReactJS.

Saya mencoba menulis aplikasi ini sesederhana mungkin. Harapannya agar bisa diikuti siapa saja. Di aplikasi ini, Saya hanya menggunakan default state React, Bootstrap, dan SDK Firebase.

Aplikasi yang kita buat nantinya seperti ini. Demo.

User bisa daftar dengan akun email atau gmail mereka, lalu bisa melakukan aktivitas login, membaca session, dan logout.

Di sini kita akan menggunakan Firebase sebagai backend authentikasi user.

Saya sendiri memakai TypeScript untuk tutorial ini. Tetapi jangan khawatir, karena kode TypeScript sangat mirip dengan JavaScript.

Install Dependency

yarn create react-app user-session-demo --template typescript
yarn add typescript @types/node @types/react @types/react-dom @types/jest

Install Firebase SDK, lihat dokumentasi lengkap.

yarn add firebase bootstrap

Struktur direktori aplikasi yang akan kita buat akan seperti ini:

├── src
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── components
│   │   ├── Login.tsx
│   │   ├── Signup.tsx
│   │   └── Welcome.tsx
│   ├── firebase.ts
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   ├── serviceWorker.ts
│   ├── setupTests.ts
│   └── validator.ts
...

App.tsx akan menjadi base dari aplikasi kita, selanjutnya components/Login.tsx, components/Signup.tsx, dan components/Welcome.tsx masing-masing akan menjadi page yang akan kita buat untuk aplikasi ini.

Static Component

Ada banyak cara untuk meng-approach bagaimana membuat apliakasi. Yang paling sering saya pakai salah satunya adalah component first. Saya membuat component statis terlebih dahulu, baru kemudian menambahkan logic.

Keuntungan dari pendekatan ini adalah, kita bisa memvisualisasikan secara detail bagaimana aplikasi kita akan dibuat. Sehingga nantinya kita tahu fungsi-fungsi apa yang kita perlukan.

Pendekatan ini juga ideal dilakukan, jika yang mengerjakan aplikasi masih sedikit.

Okey. Pertama kita akan membuat komponen statis dari App.tsx, Login.tsx, Signup.tsx, dan Welcome.tsx.

import React from 'react';
import './App.css';
import { Login } from './components/Login';
import { Signup } from './components/Signup';
import { Welcome } from './components/Welcome';

function App() {
  return (
    <div className="App">
      <div className="container mt-5">
        <div className="row justify-content-md-center">
          <div className="col-sm-4">
            <Login />
            <Signup />
          </div>
          <div className="col-sm-6">
            <Welcome />
          </div>
        </div>
      </div>
    </div>
  );
}

export default App;
App.tsx
import React from "react";

export const Login = (props: any): JSX.Element => {
  return (
    <>
      <div className="alert alert-danger" role="alert">
        A simple danger alert—check it out!
      </div>

      <div className="border rounded shadow-sm p-3 mb-5 bg-white rounded">
        <form className="text-left">
          <div className="form-group">
            <label htmlFor="exampleInputEmail1">Email address</label>
            <input type="email" className="form-control form-control-lg" id="exampleInputEmail1" aria-describedby="emailHelp" />
            <small id="emailHelpLogin" className="form-text text-danger">We'll never share your email with anyone else.</small>
          </div>
          <div className="form-group">
            <label htmlFor="exampleInputPassword1">Password</label>
            <input type="password" className="form-control form-control-lg" id="exampleInputPassword1" />
            <small id="passwordHelpLogin" className="form-text text-danger">We'll never share your email with anyone else.</small>
          </div>
          <div className="form-group form-check">
            <input type="checkbox" className="form-check-input" id="exampleCheck1" />
            <label className="form-check-label" htmlFor="exampleCheck1">Remember Me</label>
          </div>
          <button disabled type="button" className="btn btn-primary btn-lg btn-block">Submit</button>
        </form>
        <div className="my-3"><em>Or</em></div>
        <button disabled type="button" className="btn btn-link btn-lg btn-block mb-3">Login with Google</button>
        <div className="border-top my-3" />
        <span>Don't have an account yet? <button className="btn btn-light btn-sm">Register here</button></span>
      </div>
    </>
  );
};
components/Login.tsx
import React from "react";

export const Signup = (props: any): JSX.Element => {
  return (
    <>
      <div className="alert alert-danger" role="alert">
        A simple danger alert—check it out!
      </div>

      <div className="border rounded shadow-sm p-3 mb-5 bg-white rounded">
        <form className="text-left">
          <div className="form-group">
            <label htmlFor="exampleInputEmail1">Email address</label>
            <input type="email" className="form-control form-control-lg" id="exampleInputEmail1" aria-describedby="emailHelp" />
            <small id="emailHelpRegister" className="form-text text-danger">We'll never share your email with anyone else.</small>
          </div>
          <div className="form-group">
            <label htmlFor="exampleInputPassword1">Password</label>
            <input type="password" className="form-control form-control-lg" id="exampleInputPassword1" />
            <small id="passwordHelpRegister" className="form-text text-danger">We'll never share your email with anyone else.</small>
          </div>
          <button disabled type="button" className="btn btn-primary btn-lg btn-block">Submit</button>
        </form>
        <div className="border-top my-3" />
        <span>Have an account? <button className="btn btn-light btn-sm">Login here</button></span>
      </div>
    </>
  );
};
components/Signup.tsx
import React from "react";

export const Welcome = (props: any): JSX.Element => {
  return (
    <div className="jumbotron text-left">
      <h1 className="display-4">Hello, Name</h1>
      <p className="lead">This is a simple hero unit, a simple jumbotron-style component for calling extra attention to featured content or
        information.</p>
      <hr className="my-4" />
      <p>It uses utility classes for typography and spacing to space content out within the larger container.</p>
      <button className="btn btn btn-outline-danger">Sign Out</button>
    </div>
  );
};
components/Welcome.tsx

Sampai di sini, tampilan aplikasi statis kita sudah selesai. Tampilanya kira-kira seperti di bawah:

Komponen Login dan Welcome statis
Komponen Login dan Welcome statis

Setup Firebase

Bagian selanjutnya setelah kita memiliki UI, kita akan menyiapkan logic untuk dipakai UI. Jika kita lihat dari tampilan statisnya. Aplikasi yang akan kita buat memerlukan logic untuk:

  • Sign-in email password
  • Sign-in Google account
  • Sign-up email password
  • Persist user session.
  • Logout

Agar logic di atas bisa berjalan sperti yang kita harapkan. Langkah pertama yang perlu kita lakukan adalah activate fitur yang kita melalui Firebase Console.

Firebase Console -> Authentication -> Sign-in method

Activate Email Login

Activate Email Login

Activate Google Login

Activate Google Login

Firebase Authentication

Tahap selanjutnya setelah kita aktivasi beberapa fitur di Firebase Console adalah membuat logic yang akan dipakai untuk UI. Saya akan mengumpulkan semua logic ini ke dalam satu file yang bernama firebase.ts.

Kamu juga bisa membaca Getting Started with Firebase untuk dokumentasi yang lebih detail.

Beberapa namespace yang perlu kita import:

import * as firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
firebase.ts

Selanjutnya, agar Firebase bisa berkomunikasi dengan Aplikasi yang kita buat. Kita harus meng-copy konfigurasi Firebase ke dalam aplikasi kita.

const firebaseConfig = {
  apiKey: "AIzaSyCRAXsdsadnfO4xas2sWiqwzaZPGQbe7pmQ",
  authDomain: "remote-config-demo-3eb4c.firebaseapp.com",
  databaseURL: "https://remote-config-demo-4eb4c.firebaseio.com",
  projectId: "remote-config-demo-4eb4c",
  storageBucket: "remote-config-demo-4eb4c.appspot.com",
  messagingSenderId: "232509884211",
  appId: "1:232509884210:web:350d48e117af09a89d792b"
};

firebase.initializeApp(firebaseConfig);
type UserCredential = firebase.auth.UserCredential;
const Persistence = firebase.auth.Auth.Persistence;
firebase.ts

Pertama, saya akan membuat handler untuk membuat user baru dengan Email dan Password.

Nah, Firebase sendiri sudah memiliki fungsi tersebut dengan nama firebase.auth().createUserWithEmailAndPassword, yang akan kita lakukan adalah me-wrap fungsi tersebut ke-dalam fungsi aplikasi kita.

Di sini saya menambahkan parameter tambahan berupa callback function yang menerima dua argumen, response dan error.

Response akan kita kasih ketika kita berhasil membuat user di Firebase, dan error akan kita kasih ketika kita gagal membuat user di Firebase.

export const createUserWithEmailAndPassword = async (email: string, password: string, callback: (response?: UserCredential, err?: Error) => void): Promise<void> => {
  try {
    const res: UserCredential = await firebase.auth().createUserWithEmailAndPassword(email, password);
    callback(res);
  } catch (e) {
    callback(undefined, e);
  }
};
firebase.ts

Selanjutnya saya akan membuat fungsi signInWithEmailAndPassword. Fungsi ini secar flow hampir mirip dengan createUserWithEmailAndPassword. Bedanya ada di bagian rememberMe.

Jika user berkehendak untuk menyimpan session-nya untuk jangka waktu yang lama, degan memilih rememberMe, kita akan memberikan argumen tambahan ke Firebase dengan value firebase.auth.Auth.Persistence.LOCAL.

Value firebase.auth.Auth.Persistence.LOCAL akan menyimpan session user selama mungkin, sampai akhirnya user sendiri yang menghancurkan sesion tersebut dengan cara logout.

Kamu bisa membaca lebih lengkap tentan session presistence di dokumntasi Firebase.

export const signInWithEmailAndPassword =  async (email: string, password: string, rememberMe: boolean, callback: (response?: UserCredential, err?: Error) => void): Promise<void> => {
  const persistence = rememberMe ? Persistence.LOCAL : Persistence.SESSION;
  try {
    await firebase.auth().setPersistence(persistence);
    const res: UserCredential = await firebase.auth().signInWithEmailAndPassword(email, password);
    callback(res);
  } catch (e) {
    callback(undefined, e);
  }
};
firebase.ts

Handler sign-in dengan Google Account:

export const onAuthStateChanged = firebase.auth().onAuthStateChanged;

const provider = new firebase.auth.GoogleAuthProvider();
provider.addScope('');

export const signInWithGoogle = async (callback: (err?: Error) => void): Promise<void> => {
  try {
    await firebase.auth().signInWithRedirect(provider);
    callback();
  } catch (e) {
    callback(e);
  }
};
firebase.ts

Handler sign out

export const signOut: () => Promise<void> = firebase.auth().signOut;

Handle Session Change

export const onAuthStateChanged = firebase.auth().onAuthStateChanged;

Wired Up Firebase and UI

Handle Sign Up

Setelah kita selesai myiapkan logic Firebase. Selanjutnya kita akan membuat kode agar UI dan logic firebase bisa ber-komuniasi.

Untuk bagian sign-up setiaknya kita memerlukan state email, password, dan error. State loading sifatnya optional.

  const [email, setEmail] = useState<string>("");
  const isEmailValid = validateEmail(email);
  const [password, setPassword] = useState<string>("");
  const isPasswordValid = validatePassword(password);
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<Error | null>(null);
component/Signup.tsx

Untuk validasinya email dan password, saya hanya memakai regex.

export const validateEmail = (email: string = ""): boolean => {
  return /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,})+$/.test(email);
};

export const validatePassword = (password: string = ""): boolean => {
  return /^[^%]{6,}$/.test(password);
};
validator.ts

Dan berikut adalah handler yang saya gunakan di onChange event dari input password dan email. Cukup straight forward saya rasa.

(event) => {
    event.preventDefault();
    const text = event.target.value;
    setPassword(text);
 }
 
 (event) => {
 	event.preventDefault();
    const text = event.target.value;
    setEmail(text);
 }
component/Signup.tsx

Nah, di bagian handle onClick even button. Pertama saya akan melakukan clearance error dahulu jika ada. Tujunnya agar pesar error hilang dan diganti yang baru jika ada.

Selanjutnya saya inisialisasi marker loading di bagian UI. Baru setelah itu memanggil fungsi createUserWithEmailAndPassword.

(event) => {
	setError(null);
	setLoading(true);
    createUserWithEmailAndPassword(email, password, (res, err) => {
        if (res) setLoading(false);
        if (err) {
            setLoading(false);
            setError(err);
        }
	});
}
component/Signup.tsx

Jika registrasi berhasil, record user yang berhasil registrasi akan muncul di Dashboard Firebase.

Firebase signup record

Jika registrasi gagal, kita akan menampilkan pesar error kenapa user bersangkutan belum bisa melakukan registrasi.

{ error ?
        <div className="alert alert-danger" role="alert">
          { error?.message }
        </div> : null }
component/Signup.tsx

Handle Sign In

Bagian handle sign-in tidak jauh berbeda dengan handle-sign up. Bedanya di bagian handle sign ini ada state tambahan yang bernama rememberMe.

const [email, setEmail] = useState<string>("");
const isEmailValid = validateEmail(email);
const [password, setPassword] = useState<string>("");
const isPasswordValid = validatePassword(password);
const [rememberMe, setRememberMe] = useState<boolean>(true);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);
component/Login.tsx

Handle Change

(event) => {
	event.preventDefault();
	const text = event.target.value;
    setEmail(text);
}

(event) => {
	event.preventDefault();
	const text = event.target.value;
	setPassword(text);
}
component/Login.tsx

Handle Click Sign-in

(event) => {
	setLoading(true);
    setError(null);
    signInWithEmailAndPassword(email, password, rememberMe, (response, err) => {
		if (response) setLoading(false);
		if (err) {
			setLoading(false);
			setError(err);
		}
	});
}
component/Login.tsx

Handle Sign-in with Google

(event) => {
	signInWithGoogle((err) => {
		if (err) setError(err);
	});
}
component/Login.tsx

Handle Error

{ error ?
        <div className="alert alert-danger" role="alert">
          { error?.message }
        </div> : null }
component/Login.tsx
Bagaimana kita bisa mengganti UI aplikasi yang kita buat sesuai dengan session yang dimiliki user?

Untuk menjawab pertanyaan itu, kita akan me-listen kedalam sebuah state dari firebase. State ini akan berbuah secara realtime ketika session user berubah. Untuk me-listen state ini, Firebase menyediakan fungsi auth.onAuthStateChanged.

Fungsi auth.onAuthStateChanged menerima satu parameter callback yang memberikan state terakhir dari user.

useEffect(() => {
  const unsubscribe = auth.onAuthStateChanged((user: User | null) => {
    if (user) {
      setUser(user);
      setPath("welcome");
    }
    setLoading(false);
  });
  return () => {
    unsubscribe();
  };
});
component/App.tsx

Karena auth.onAuthStateChanged ini sifatnya Side Effect, saya membungkusnya kedalam react function useEffect.

useEffect adalah function yang React sediakan untuk memanggil fungsi-fungsi yang bersifat side effect. Dengan useEffect selanjutnya kita juga bisa melakukan clean up dari side effect yang kita buat.

Di fungsi di atas. saya Melakukan clean up dengan cara memangil unsubscribe yang dikembalikan dari fungsi auth.onAuthStateChanged.

Router

Idealnya aplikasi yang kita buat memiliki route untuk setiap page yang dikunjungi user. Mungkin di lain kesempatan saya akan menulis tetnang react router.

Apa yang akan saya lakukan adalah membuat sebuah mock router. Mock router ini selanjutnya akan saya simpak ke dalam state agar bisa disesuaikan UI.

type Path = ("welcome" | "sign-up" | "sign-in")
component/App.tsx
const [user, setUser] = useState<User | null>(null);
const [path, setPath] = useState<Path>("sign-in");
const setSignUpPath = () => setPath("sign-up");
const setSignInPath = () => setPath("sign-in");
const [loading, setLoading] = useState<boolean>(true);
component/App.tsx

Note di sini. Saya memberikan props tambahan ke komponen Signup dan Login. Poperti ini berupa callback untuk mengganti route. UI akan menyesuaikan tampilan sesuai route yang kita pilih. Entah itu "sign-in", "sign-up", atau "welcome".

<div className="App">
  <div className="container mt-5">
    <div className="row justify-content-md-center">
      { path === "sign-in" ?
        <div className="col-sm-4">
          <Login setSignUp={setSignUpPath}/>
        </div> : null }
      { path === "sign-up" ?
        <div className="col-sm-4">
          <Signup setSignIn={setSignInPath}/>
        </div> : null }
      { user ?
        <div className="col-sm-8">
          <Welcome user={user}/>
        </div> : null }
    </div>
  </div>
</div>
component/App.tsx

Welcome Page

Tidak ada yang spesial di halaman welcome. Halaman ini hanya menampilkan nama atau email user yang dilempar oleh Firebase.

const Welcome = (props: {user: User}): JSX.Element => {
  return (
    <div className="jumbotron text-left">
      <h1 className="display-3">Hello,</h1>
      <h2 className="display-4">{props.user.displayName || props.user.email}</h2>
      <p className="lead">This is a simple hero unit, a simple jumbotron-style component for calling extra attention to featured content or
        information.</p>
      <hr className="my-4" />
      <p>It uses utility classes for typography and spacing to space content out within the larger container.</p>
      <button
        className="btn btn btn-outline-danger"
        onClick={(event) => {
          auth.signOut().then(() => {
            window.location.reload();
          });
        }}
      >Sign Out</button>
    </div>
  );
};
component/Welcome.tsx

Note di bagian akhir. Untuk logout, saya memanggil fungsi firebase.auth().signOut lalu me-refresh halaman untuk memastikan session sudah berhasil di-destroy.


Selesai! Kamu bisa melihat aplikasi keseluruhannya di demo ini. Atau kamu juga bisa membaca kode-kode di atas di Github saya.

Fiuh.. Cape juga ya. Tapi saya sangat puas dengan kode ini. Awalnya saya berpikir untuk menggunakan Redux dan React-Router untuk menghandle satate dan session login.

Tetapi kemudian, saya rasa, bare-minimum aplikasi yang well functional lebih mudah dibaca dan dimengerti. Untuk konsep-konsep advance akan mengikuti setelah mengerti bagaimana end-to-end bisa dipahami.

Simple made easy, isn't right? 😁