ทำระบบ Login/Register ด้วย React Router และ Firebase Auth (JavaScript)

React Sep 23, 2023

ตัวอย่าง Workshop วิธีการทำระบบสมัครสมาชิก และเข้าสู่ระบบ โดยใช้ React.js ร่วมด้วย React Router และ Firebase Authentication โดยเนื้อหานี้ออกแบบมาสำหรับเรียนด้วยการลงมือทำ ฝึกฝน ตามตัวอย่างครับ อาจจะไม่ได้อธิบายลงลึกในแต่ละเนื้อหา ไม่จำเป็นต้องเข้าใจ 100% ครับ เน้นลงมือทำ ลงมือฝึกฝน และค่อยๆ เรียนรู้ไปครับ

สำหรับเนื้อหาใน workshop นี้จะแบ่งออกเป็น 2 เวอร์ชั่นคือ

  1. เนื้อหา React Router + Firebase Authentication (JavaScript) (บทความนี้)
  2. เนื้อหา React Router + Firebase Authentication (TypeScript)

ระดับความยาก: ⭐️⭐️

เหมาะสำหรับผู้ที่มีพื้นฐาน React มาบ้างแล้ว และอยากลองทำระบบ Login / Register รวมถึงเข้าใจขั้นตอนการจัดการ Client Routing

สิ่งที่จะได้รับ:

  • สามารถทำระบบ Login/Register ด้วย Firebase Auth เบื้องต้นได้
  • เข้าใจการกำหนด Routing แบบ Client ด้วย React Router
  • การ Deploy โปรเจ็คขึ้น Vercel และ Firebase Hosting
  • Source Code ของโปรเจ็ค เพื่อนำไปเรียนรู้ ทบทวน และฝึกฝน
ตัวเนื้อหาสำหรับ Pro Member นะครับ

ระบบที่เราจะสร้างวันนี้ คือ

  1. มีหน้าสมัครสมาชิก (Register)
  2. มีหน้าเข้าสู่ระบบ (Login)
  3. หน้า Dashboard ที่จะโชว์ข้อมูลเฉพราะเราเข้าสู่ระบบแล้วเท่านั้น เช่น แสดง Email ของผู้ใช้งาน

ตัวอย่าง

โดยที่หน้า Dashboard หากไม่ได้เข้าสู่ระบบ จะไม่สามารถดูเนื้อหาได้

แบบ Video Clip สั้นๆ

0:00
/0:36

เทคโนโลยีที่เกี่ยวข้อง

  • Package Manager ที่ชอบ ในบทความใช้ pnpm (สามารถใช้ npm, yarn หรือ bun แทนได้เช่นกัน)
  • React และ Vite
  • React Router เวอร์ชั่น v.6.16.0 ณ ที่เขียนบทความ
  • Firebase JavaScript SDK
  • Vercel - สำหรับการ Deploy ตัวอย่างโปรเจ็ค

เนื้อหาในบทเรียนนี้


Step 1 - เริ่มต้นสร้างโปรเจ็ค

เริ่มต้น เราจะสร้างโปรเจ็คขึ้นมาด้วย Vite กันนะครับ เริ่มจากเปิด Terminal พิมพ์คำสั่งนี้ลงไป (สามารถใช้ npm, pnpm, yarn หรือ bun ก็ได้)

pnpm create vite@latest

จากนั้นเลือกตั้งชื่อไฟล์, เลือก Library/Framework ตามนี้ (ผมตั้งชื่อว่า react-firebase-auth-js เพื่อนๆ สามารถตั้งชื่อโปรเจ็คได้ตามต้องการ)

✔ Project name: … react-firebase-auth-js
✔ Select a framework: › React
✔ Select a variant: › JavaScript

ทดสอบโปรเจ็คของเรา ว่าสร้างถูกต้องหรือไม่ โดยการไปที่โฟลเดอร์ที่สร้างขึ้นมา ทำการ install dependencies และลอง start dev server

  cd react-firebase-auth-js
  pnpm install
  pnpm run dev

จะได้ผลลัพธ์ และหน้าเว็บ http://localhost:5174

  VITE v4.4.5  ready in 188 ms

  ➜  Local:   http://localhost:5174/
  ➜  Network: use --host to expose
  ➜  press h to show help

Step 2 - สร้างโปรเจ็คใน Firebase

เข้าหน้าเว็บ Firebase หากยังไม่มีบัญชีก็ต้องทำการสมัครสมาชิกของ Firebase ก่อน จริงๆ แล้วตัว Firebase มี Service ให้บริการมากมายครับ แต่สำหรับบทความนี้ เราจะพูดถึง และใช้งานแค่ Service เดียว นั่นก็คือ Firebase Authentication

จากนั้น เมื่อมีบัญชี Firebase แล้ว ให้เราเข้าไปที่หน้า Firebase Console ทำการสร้างโปรเจ็คขึ้นมาใหม่ (ตั้งชื่อ Project Name)

ตรง Google Analytics เราสามารถ turn off ได้ (แต่ถ้าใครต้องการ ก็กดเลือก Turn On แล้วก็ผูกกับบัญชี Google Analytics ของตัวเองได้ครับ)

ทำการกด Create a Project

เมื่อโปรเจ็คสร้างเสร็จแล้ว ให้กดเข้าไปหน้า Project จากนั้นเลือก ตรง Web ทำการ Register App

ต่อมา เราจะทำการเพิ่ม Firebase SDK ตามที่หน้า Firebase Console แนะนำ เลยครับ ก็คือติดตั้ง dependencies และสร้างไฟล์ config แบบภาพด้านล่าง

แหล่งอ้างอิงสำหรับ Firebase และ Firebase Authentication

Firebase Authentication
Firebase Authentication lets you add an end-to-end identity solution to your app for easy user authentication, sign-in, and onboarding in just a few lines of code.
Get Started with Firebase Authentication on Websites

Step 3 - Setup Firebase SDK

กลับมาที่ตัวโค๊ดของเรา หลังจากที่เราได้ config จากหน้า Firebase Console แล้ว ให้เรามาสร้างไฟล์ ชื่อ libs/firebase.js ขึ้นมา จากนั้นเซฟ config ของเราลงไป (เพิ่ม export เอาไว้ใช้ด้วย)

// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app"
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "AI....",
  authDomain: "....",
  projectId: "....",
  storageBucket: "....",
  messagingSenderId: "....",
  appId: "1:...."
}

// Initialize Firebase
const app = initializeApp(firebaseConfig)

export default app

ต่อมา เราจะย้าย พวกไฟล์ config ไปไว้เป็น .env ไม่อยากใส่พวก apiKey ไว้ที่ตรงนี้ครับ เราสามารถย้ายไปไว้ที่ไฟลื .env ได้ และตัว Vite ก็ซัพพอร์ตโดยที่เราไม่ต้องทำอะไรเพิ่มเลย เพียงแค่ต้องตั้งชื่อขึ้นต้นด้วย VITE_*

ในไฟล์ .env ผมจะมีเป็นแบบนี้

VITE_FIREBASE_API_KEY=xxx
VITE_FIREBASE_AUTH_DOMAIN=xxx
VITE_FIREBASE_PROJECT_ID=xxx
VITE_FIREBASE_APP_ID=xxx

จากนั้น วิธีการโหลด env ของ vite จะใช้เป็น import.meta.env.<NAME> ก็แก้ไขไฟล์ firebase.js เป็นแบบนี้

// Import the functions you need from the SDKs you need
import { initializeApp } from 'firebase/app'
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
}

// Initialize Firebase
const app = initializeApp(firebaseConfig)

export default app

สังเกตว่าผมเอา storageBucket และ messagingSenderId เพราะว่ายังไม่ได้ใช้ในการทำ Firebase Auth นั่นเอง

ต่อมา ทำการติดตั้ง Firebase SDK เพื่อใช้ firebase ในโปรเจ็คของเรา:

pnpm install firebase

จากนั้น กลับไปหน้า Firebase Console อีกครั้ง เลือกตรง Authentication และทำการเลือก sign-in method ตัว Firebase Authentication รองรับการ sign-in หลายแบบครับ ทั้ง Facebook, Google, Twitter หรืออื่นๆ แต่ตัวอย่างนี้ เราจะใช้แค่แบบพื้นฐาน คือ Email/Password

เลือกเป็น Email/Password และ กด Enable และเซฟ

ทีนี้ เราเลือก service Firebase Authentication ฉะนั้น เราจะทำการเพิ่ม product ที่ไฟล์ libs/firebase.js โดยเพิ่ม auth ลงไปแบบนี้

import { initializeApp } from "firebase/app"

// 1. เรียกใช้ service firebase/auth
import { getAuth } from "firebase/auth"

const app = initializeApp(firebaseConfig)

// 2. ใช้ service auth ผ่าน named import
export const auth = getAuth(app)

export default app

ตัว auth นี้ ต่อไป เราจะใช้ในการเรียก function ของ Firebase ต่อไปครับ

Step 4 - กำหนด Route ด้วย React Router

ขั้นตอนนี้ คือการกำหนด Client Route เพื่อให้เว็บเราสามารถแสดงหน้า แต่ละหน้า แตกต่างกันได้ เช่นจะแบ่งเป็นหน้าแรก หน้าสมัครสมาชิก หน้าเข้าสู่ระบบ และหน้า Dashboard หลังจากเข้าสู่ระบบเรียบร้อยแล้ว

ทำการติดตั้ง React Router ด้วยคำสั่ง

pnpm install react-router-dom

ต่อมา จะเป็นการเพิ่มหน้าเพจ โดยสร้างเป็น component แยก ไว้ที่โฟลเดอร์ pages (เป็นแค่ convention นะครับ ไม่ได้มีความหมายอะไร จะไว้ใน app ใน components ก็ได้)

โดยเราจะสร้างหน้าสมัครสมาชิกขึ้นมา เป็น url /register โดยสร้างไฟล์ /src/pages/register.jsx ขึ้นมา จากนั้นก็เป็นหน้าธรรมดาๆ ก่อน

import React from 'react'

const Register = () => {
  return <p>Register</p>
}

export default Register

สร้างไฟล์สำหรับหน้า login เผื่อเอาไว้เช่นกัน ข้างในก็ยังเป็นแค่ skeleton ชื่อ src/pages/login.jsx

import React from 'react'

const Login = () => {
  return <p>Login</p>
}

export default Login
สังเกตว่าทำไมผมตั้งชื่อ component เป็นตัวพิมพ์เล็ก? จริงๆ แล้วไม่มีกฎตายตัว สามารถตั้ง register.jsx หรือ Register.jsx ก็ได้ แต่ผลตั้งชื่อไฟล์ ตัวเล็กหมด เนื่องจาก บาง OS หรือ git มันจะเป็น case-insensitive (ค่า default) ตัวเล็ก ตัวใหญ่ มองเป็นไฟล์เดียวกัน ฉะนั้นก็เลยตั้งเป็นตัวเล็กหมด เพื่อตัดปัญหา เฉยๆ

กำหนด router ด้วย createBrowserRouter() เพื่อระบุว่า path นั้นๆ จะทำการ render component อะไร

สามารถอ่านเรื่อง วิธีการเลือกใช้ Router ได้จากบทความนี้ได้ครับ

วิธีการกำหนด Routes ของ React Router v6
เนื่องจากช่วงนี้ได้กลับมาทบทวน React Router เพื่อนำมาทำเป็นเนื้อหาในคอร์สสอน React เบื้องต้น หลังจากที่ไม่ได้ใช้ Client Routing มานานมากๆ แล้ว (ปัจจุบัน แอพ React ทั้งหมด ผมใช้ Next.js ทั้งสิ้น) วันนี้ลองไปนั่งอ่าน Doc ทบทวน ก็เลยนำมาเขี

สร้างไฟล์ router.jsx ขึ้นมา ข้างในกำหนด routing ไว้แบบนี้

import React from 'react'

import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
} from 'react-router-dom'

import Login from './pages/login'
import Register from './pages/register'

const router = createBrowserRouter(
  createRoutesFromElements(
    <>
      <Route path="/" element={<p>Home</p>} />
      <Route path="login" element={<Login />} />
      <Route path="register" element={<Register />} />
    </>
  )
)

export default router

โดยตอนนี้เรากำหนด routes ไว้ 3 หน้าคือ

  • / - หน้า Home
  • /login - คือหน้า สำหรับเข้าสู่ระบบ
  • /register - คือหน้าสำหรับสมัครสมาชิก

จากนั้นที่ไฟล์ App.jsx เราจะลบโค๊ด default ทั้งหมด และเหลือไว้แค่นี้

import { RouterProvider } from 'react-router-dom'
import './App.css'

import router from './router'

function App() {
  return <RouterProvider router={router} fallbackElement={null} />
}

export default App

ทดสอบด้วยการเข้าหน้าเว็บ http://localhost:5173/ , http://localhost:5173/login และ http://localhost:5173/register เพื่อดูผลลัพธ์ ต้องเห็นหน้าตาเว็บ ที่เป็น default component ที่เราทำไว้ แสดงว่าเราตั้งค่า Router ถูกต้องแล้ว

ถ้าขึ้นหน้าแบบนี้ แสดงว่าถูกต้อง

Step 5 - ทำหน้า Register

ปรับแต่งหน้า Register เพิ่ม Markup และ Style ลงไป (ขั้นตอนนี้ผมจะไม่ได้อธิบายรายละเอียดนะครับ ว่ากำหนด div กำหนด style ยังไงบ้าง เราจะโฟกัสกันที่ตัว React และ การทำงานของมันเนอะ ส่วนนี้เพื่อนๆ สามารถปรับแต่ง ได้ตามความเหมาะสม หรือปรับตามที่ตัวเองต้องการได้ เช่นกัน)

สำหรับไฟล์ css ในโปรเจ็ค ใช้ที่มากับ default ของ vite นะครับ คือ App.css และ index.css

ตัวไฟล์ App.css ลบไปเกือบหมด เหลือไว้แค่นี้

#root {
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

และไฟล์ index.css ครับ มีเพิ่มเติมจาก default ไปนิดหน่อย ตรง .form-container

:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  color-scheme: light dark;
  color: rgba(255, 255, 255, 0.87);
  background-color: #242424;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
}

a {
  font-weight: 500;
  color: #646cff;
  text-decoration: inherit;
}
a:hover {
  color: #535bf2;
}

body {
  margin: 0;
  display: flex;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
}

button {
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 2em;
  width: 100%;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #1a1a1a;
  cursor: pointer;
  transition: border-color 0.25s;
}
button:hover {
  border-color: #646cff;
}
button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}

/* Login & Register */
.form-container {
  display: flex;
  max-width: 840px;
  margin: 0 auto;
  flex-direction: column;
}

.form-container .container {
  display: flex;
  flex-direction: column;
  width: 240px;
  row-gap: 1.5rem;
  text-align: left;
}

.form-container input {
  padding: 0.5rem;
}

.error-message {
  color: #ff4643;
}

ทีนี้ ในส่วนโค๊ด pages/register.jsx ผมอธิบาย เพิ่มเติม และดูโค๊ดด้านล่างประกอบนะครับ

  1. มี <form> เมื่อกด submit เราจะก็ไปเรียก function handleSubmit เพื่อ ไปเรียก function ของ firebase auth อีกที
  2. ส่วนของ <input> มีแค่ 2 ตัว คือ Email และ Password โดยใช้ เป็นแบบ uncontrolled form ด้วยการใช้ React.useRef
import { useRef } from 'react'

const Register = () => {
  const emailRef = useRef()
  const passwordRef = useRef()

  const handleSubmit = async (e) => {
    e.preventDefault()
  }

  return (
    <form onSubmit={handleSubmit} className="form-container">
      <h2>สมัครสมาชิก</h2>
      <div className="container">
        <label htmlFor="username">Email</label>
        <input ref={emailRef} type="email" name="username" required />

        <label htmlFor="password">Password</label>
        <input ref={passwordRef} type="password" name="password" required />

        <button type="submit">Register</button>
      </div>
    </form>
  )
}

export default Register
  • ใช้ useRef() เป็น emailRef และ passwordRef กับ <input ref={} เพื่อเอาไปใช้อ้างอิง ในส่วน handleSubmit
  • ในส่วน handleSubmit เนี่ย เราจะเรียก function ของ Firebase Auth สำหรับ register คือ function ชื่อว่า createUserWithEmailAndPassword
import { createUserWithEmailAndPassword } from 'firebase/auth'
import { auth } from '../libs/firebase'

// เรียก function ของ firebase โดยส่ง auth, email และ password ไป
await createUserWithEmailAndPassword(auth, email, password)

สังเกตเห็นว่า เราสามารถเรียก function ของ Firebase Auth ได้ 2 แบบคือ

แบบ Modular ส่งค่า auth เป็น argument ตัวแรก

import { getAuth, createUserWithEmailAndPassword } from "firebase/auth";
const auth = getAuth();

createUserWithEmailAndPassword(auth, email, password)

แบบ Namespace API - เรียก firebase.auth() ได้เลย

firebase.auth().createUserWithEmailAndPassword(email, password)

ข้อดีของ Modular คือตัวโปรเจ็คมีขนาดเล็กกว่าในขณะที่ตัว namespace จะเป็น api แบบเก่า และใช้มานานแล้ว มีขนาดใหญ่ เพราะรวมทุกๆ feature เลย

import { useRef } from 'react'
import { useNavigate } from 'react-router-dom'

import { createUserWithEmailAndPassword } from 'firebase/auth'
import { auth } from '../libs/firebase'

const Register = () => {
  const emailRef = useRef()
  const passwordRef = useRef()

  const navigate = useNavigate()

  const handleSubmit = async (e) => {
    e.preventDefault()

    const email = emailRef.current.value
    const password = passwordRef.current.value
  
    // 1. ทำการสร้าง user จาก email, password
    await createUserWithEmailAndPassword(auth, email, password)

    // 2. ถ้า success จะ redirect ไปหน้า /dashboard.
    navigate('/dashboard')
  }

  return (
    <form onSubmit={handleSubmit} className="form-container">
      <h2>สมัครสมาชิก</h2>
      <div className="container">
        <label htmlFor="username">Email</label>
        <input ref={emailRef} type="email" name="username" required />

        <label htmlFor="password">Password</label>
        <input ref={passwordRef} type="password" name="password" required />

        <button type="submit">Register</button>
      </div>
      {errorMessage && <p className="error-message">{errorMessage}</p>}
    </form>
  )
}

export default Register

ทดสอบ ลองเข้าหน้า Register และใส่ Email พร้อมตั้งรหัสผ่าน ลองกด Submit ดู Console log จะเห็นผลลัพธ์แบบนี้

_UserCredentialImpl {user: _UserImpl, providerId: null, _tokenResponse: {…}, operationType: 'signIn'}

และลองเข้าไปดูข้อมูลของเราที่ Firebase Console จะเห็นข้อมูลของเราอยู่ในนี้แล้ว

มีข้อมูลของเราที่สมัคร รู้ว่าสมัครโดยใช้ email, มี UID ที่ตัว firebase generated ให้

ลองทดสอบ สมัครสมาชิก ด้วย Email เดิม และดูว่าจะมี Error อะไรเกิดขึ้น?

จะได้ Error ว่า

FirebaseError: Firebase: Error (auth/email-already-in-use).

ต่อมา เราจะ handle state หลังจาก register เรียบร้อยแล้ว 2 กรณีคือ

  1. กรณี มี Error ก็จะแสดง Error ให้ User ได้รู้
  2. กรณีสมัครเรียบร้อย จะถูก redirect ไปหน้า Dashboard (หน้านี้ยังไม่ได้ทำ)
  const [errorMessage, setErrorMessage] = useState('')

  const handleSubmit = async (e) => {
    e.preventDefault()

    const email = emailRef.current.value
    const password = passwordRef.current.value

    try {
      await createUserWithEmailAndPassword(auth, email, password)
      navigate('/dashboard')
    } catch (error) {
      console.log(error)
      setErrorMessage(error.message)
    }
  }

เมื่อมีข้อมูลเวลาสมัครสมาชิก ต่อไป ลองไปดูเรื่อง Login กันต่อเลย

สำหรับคนที่อาจจะตามไม่ทัน ตอนนี้ไฟล์ของ pages/register.jsx เป็นแบบด้านล่างนะครับ

import { useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'

import { createUserWithEmailAndPassword } from 'firebase/auth'
import { auth } from '../libs/firebase'

const Register = () => {
  const emailRef = useRef()
  const passwordRef = useRef()

  const [errorMessage, setErrorMessage] = useState('')

  const navigate = useNavigate()

  const handleSubmit = async (e) => {
    e.preventDefault()

    const email = emailRef.current.value
    const password = passwordRef.current.value

    try {
      await createUserWithEmailAndPassword(auth, email, password)
      navigate('/dashboard')
    } catch (error) {
      console.log(error)
      setErrorMessage(error.message)
    }
  }

  return (
    <form onSubmit={handleSubmit} className="form-container">
      <h2>สมัครสมาชิก</h2>
      <div className="container">
        <label htmlFor="username">Email</label>
        <input ref={emailRef} type="email" name="username" required />

        <label htmlFor="password">Password</label>
        <input ref={passwordRef} type="password" name="password" required />

        <button type="submit">Register</button>
      </div>
      {errorMessage && <p className="error-message">{errorMessage}</p>}
    </form>
  )
}

export default Register

Source Code สำหรับ Part 1

Source Code

Tags

Chai Phonbopit

เป็น Web Dev ทำงานมา 10 ปีหน่อยๆ ด้วยภาษา JavaScript, Node.js, React, Vue และปัจจุบันกำลังสนใจ Web3, Crypto และ Blockchain เขียนบล็อกที่ https://devahoy.com