Tim Carlson
Spring 2024

<info 340/>

Firebase

View of the Day

  • Project Final Draft Requirements

  • Firebase Database (code demo)

Project Final Draft

For the Final Draft of the project, we're looking for it to be totally complete! See the Canvas page for full details.

  • Well-structured and appropriate HTML (in React)
  • Well-constructed React Components. Uses props and state!
  • Interactive features: "two and a half" significant features
  • Routing and navigation Needs multiple pages; url params
  • External React Library rendering a Component. react-bootstrap is okay (if used meaningfully e.g., interactive widget)
  • External Data either a fetch() request for data, or Firebase DB
    • Needs asynchronous work (effect hooks, promises)
  • Good Style, Accessibility, Responsiveness
  • Correct code style -- check the course textbook!

Project Final Draft

Final Projects will be graded in two parts:

  1. Group portion (50%): has your group implemented a complete project? Does it meet all requirements; etc.
  2. Individual mastery (50%): have you individually demonstrated that you've learned the material (HTML, CSS, React)?
    • Need git commits that show you've contributed to all these
    • If you have not demonstrated you've learned the material, you will not get full credit.
    • "Supporting the group" (helping others learn, etc) will earn extra credit. Failure to work effectively in a group (poor communication, missing deadlines, etc) will reduce score.
      This is not a measure of "how much work you did", but how well you worked with your team.
...
import { Routes, Route, Outlet, Navigate, useNavigate } from 'react-router-dom';
...
export default function App(props) {
...
const navigateTo = useNavigate(); //navigation hook
...
const loginUser = (userObj) => {
    console.log("logging in as", userObj.userName);
    setCurrentUser(userObj);
    if(userObj.userId !== null){
      navigateTo('/chat/general'); //go to chat after login
    }
  }
...
  return (
...
      <Routes>
...
        {/* protected routes */}
        <Route element={<ProtectedPage currentUser={currentUser} />}>
          <Route path="chat/:channelName" element={<ChatPage currentUser={currentUser} />} />
          {/* redirect to general channel */}
          <Route path="chat" element={<Navigate to="/chat/general" />} />
        </Route>
      </Routes>
    </div>
  );
}

function ProtectedPage(props) {
  //...determine if user is logged in
  if(props.currentUser.userId === null) { //if no user, send to sign in
    return <Navigate to="/signin" />
  }
  else { //otherwise, show the child route content
    return <Outlet />
  }
}

import Navigate and useNavigate

get the hook (useNavigate)

navigate to a specific Route

Navigate Component

Added Navigate Component

...
import { Routes, Route, Outlet, Navigate, useNavigate } from 'react-router-dom';
...
export default function App(props) {
...
 return (
    <div className="container-fluid d-flex flex-column">
      <HeaderBar currentUser={currentUser} />

      <Routes>
...

        {/* protected routes */}
        <Route element={<ProtectedPage currentUser={currentUser} />}>
          <Route path="chat/:channelName" element={<ChatPage currentUser={currentUser} />} />
          {/* redirect to general channel */}
          <Route path="chat" element={<Navigate to="/chat/general" />} />
        </Route>
      </Routes>
    </div>
  );
}
function ProtectedPage(props) {
  //...determine if user is logged in
  if(props.currentUser.userId === null) { //if no user, send to sign in
    return <Navigate to="/signin" />
  }
  else { //otherwise, show the child route content
    return <Outlet />
  }
}

If user isn't logged in, navigate to the /Signin route

 Navigate to Signin with null User

import React, { useState, useEffect } from 'react';
import { Routes, Route, Outlet, Navigate, useNavigate } from 'react-router-dom';
import { HeaderBar } from './HeaderBar.js';
import ChatPage from './ChatPage';
import SignInPage from './SignInPage';
import * as Static from './StaticPages';

import DEFAULT_USERS from '../data/users.json';

export default function App(props) {
  //state
  const [currentUser, setCurrentUser] = useState(DEFAULT_USERS[0]) //default to null user

  const navigateTo = useNavigate(); //navigation hook

  //effect to run when the component first loads
  useEffect(() => {
    //log in a default user
    loginUser(DEFAULT_USERS[1])

  }, []) //array is list of variables that will cause this to rerun if changed

first time component is rendered login a user

 Effect Hook to login user when component initialized 1st time

initialize state to the null user

export default function ChatPage(props) {
  const [chatMessages, setChatMessages] = useState(CHAT_HISTORY);
  const urlParamObj = useParams(); //get me the url parameters
  const channelList = [
    'general', 'random', 'dank-memes', 'channel-4', 'pet-pictures'
  ]

  const currentUser = props.currentUser;
  const currentChannel = urlParamObj.channelName; //get channel name from url params

  const addMessage = (messageText) => {
    const userObj = currentUser;
    const newMessage = {
...
    } 

    const updateChatMessages = [...chatMessages, newMessage];
    setChatMessages(updateChatMessages); //update state and re-render
  }

  return (
    <div className="row flex-grow-1">
      <div className="col-3">
        <ChannelList channels={channelList} currentChannel={currentChannel} />
      </div>
      <div className="col d-flex flex-column">
        <ChatPane chatMessages={chatMessages} currentChannel={currentChannel} />
        <ComposeForm currentUser={currentUser} addMessageCallback={addMessage} />
      </div>
    </div>
  )
}

ChatPane contains all the messages

 Chat Page

Chat Messages

addMessage adds message by updating state

ChatForm - user enters content calls addMessage as prop

Firebase

Firebase is a web backend solution; it provides multiple features which you can access without need to "code" them yourselves.

  • Web Hosting
  • Databases
  • User Authentication

Firebase Realtime Database

  • Real-time database for accessing data in the cloud
     
  • Can be simultaneously accessed and modified by multiple clients
     
  • Data can be edited, added to, deleted from and viewed directly within the firebase console directly
     
  • Each element of the JSON object is accessible via AJAX requests
import React from 'react';
import ReactDOM from 'react-dom/client';

import { BrowserRouter } from 'react-router-dom';
import { initializeApp } from "firebase/app"; //added from firebase
//import CSS
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';
import App from './components/App';

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "AIzaSyChR-uCZQrXIuC8l0QGQynq6D6z43cRXN8",
  authDomain: "react-chat-firebase1-temp.firebaseapp.com",
  projectId: "react-chat-firebase1-temp",
  storageBucket: "react-chat-firebase1-temp.appspot.com",
  messagingSenderId: "1052807428829",
  appId: "1:1052807428829:web:ab93f7be8ab5e149f47934"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

Demo - Changing Code to use  firebase db

index.js

Firebase Database Security Rules

  • Since all elements are accessible easily via AJAX requests, Firebase has security rules to apply credentialing for user access
     
  • Within your DB JSON tree there is a "rules" branch with .read and .write properties.
  • For our purposes we will be using "test mode" with both .read and .write set to 'true'.

Firebase Realtime Database

database object in js file

database json object in firebase

Firebase Database Reading and Writing Data

  • To get access to your database, first import the getDatabase module from 'firebase/database'
  • Then to modify values you get a reference through the ref() method
  • This method takes the key-path of the element as a parameter:

Accessing Elems - we're not using this syntax

You can use string concatenation to use variables to find specific values

You can also use the '.child()' method to reference specific child elements.

...
import { getDatabase, ref} from 'firebase/database'; //realtime database

export function ChatPane(props) {
...

const handleTestClick = (event) => {
    console.log("testing...");

    //add to database
    const db = getDatabase();
    const messageRef = ref(db, "message") //refers to the message key in the database
    console.log(messageRef);

    const profLastNameRef = ref(db, "professor/lastName");
    console.log(profLastNameRef);

  }

...
return (
    <div className="scrollable-pane">
      <div className="pt-2 my-2">
        <Button className="justify-content-start" variant="warning" onClick={handleTestClick}> Test</Button>
        <p> </p>
        {messageItemArray}
      </div>
    </div>
  )
}

import getDatabase, ref

database ref

message ref

url type path to get to subkeys

Firebase: Getting a reference to fb node

Firebase: Writing Data

You can modify an entry using the .set() method. It takes 2 arguments, the db reference and the new value

Firebase: Writing Data (new entry)

New entries also require getting the reference, and using the setter providing an object with the values to be written

...
import { getDatabase, ref, set as firebaseSet} from 'firebase/database'; //realtime database

export function ChatPane(props) {
...
  const handleTestClick = (event) => {
    
    //get database
    const db = getDatabase();
    
    const messageRef = ref(db, "message")
    
    firebaseSet(messageRef, "You clicked me!");

    const profFirstNameRef = ref(db, "professor/firstName")
    
    firebaseSet(profFirstNameRef, "Timothy");

    const profCourseRef = ref(db, "professor/courseNumber");
    
    firebaseSet(profCourseRef, "INFO 340");

  }

...
  return (
...
  )
}

Demo - Update or Create data to Firebase

import and alias 'set'

If the reference doesn't exist yet, then the node will be created when calling set!

get db reference

get ref to message

update message

firstName ref

update firstName

...
import { getDatabase, ref, set as firebaseSet} from 'firebase/database'; 
...
export default function ChatPage(props) {
...

  const addMessage = (messageText) => {
    const userObj = currentUser;
    const newMessageObj = {
      "userId": userObj.userId,
      "userName": userObj.userName,
      "userImg": userObj.userImg,
      "text": messageText,
      "timestamp": Date.now(),
      "channel": currentChannel
    } 

    // const updateChatMessages = [...chatMessages, newMessage];
    // setChatMessages(updateChatMessages); //update state and re-render
    const db = getDatabase();
    const messageRef = ref(db,"message");

    firebaseSet(messageRef, newMessageObj);

  }


  return (
  )

}

Demo - Write single message string

import libraries from fb

addMessage function in ChatPage

 Write the newMessageObj to Firebase  at the "message" key  instead of updating the state variable

Firebase: Set method is async 

The set method returns promise because it is async

Firebase : Listening for Data Changes

As a real-time DB, data can change anytime. Firebase provides event listener .onValue to allow apps to respond and update.

  • .onValue() takes two parameters: the database reference and a callback function
  • The 'value' event is most common event which registers when data is created or changes.
  • The callback function is passed back a "data snapshot" object as a param
  • This is just a wrapper around the JSON tree. Use .val()  to get the value

 

Firebase DB - Use EffectHook (and Cleanup)

  • Listener goes inside  EffectHook because it includes network access
  • Cleanup function to prevent memory leak when component removed
...
import { getDatabase, ref, set as firebaseSet, onValue} from 'firebase/database'; 
...
export default function ChatPage(props) {
...
  const db = getDatabase();
  const messageRef = ref(db, "message");
  
  useEffect(() => {
    onValue(messageRef, (snapshot) =>{
      const newValue = snapshot.val();
      console.log("firebase value changed")
      console.log(snapshot);
      console.log("new value: ", newValue)
    })
  }, []);


  const addMessage = (messageText) => {
    
...
    } 

    const db = getDatabase();
    const messageRef = ref(db,"message");
    firebaseSet(messageRef, newMessageObj);
  }

  return (
...  )
}

Demo - Adding listener for db changes

onValue event listener

Load onValue in effect hook so it loads once when Component instantiates

use the returned 'snapshot' to get the value using snapshot.val()


export default function ChatPage(props) {
  const [chatMessages, setChatMessages] = useState([]);

 ...
 
  const db = getDatabase();
  const messageRef = ref(db, "message");
  
  useEffect(() => {
    const offFunction = onValue(messageRef, (snapshot) =>{
      const newMessageObj = snapshot.val();
      console.log(newMessageObj);

     const updateChatMessages = [...chatMessages, newMessageObj];
     setChatMessages(updateChatMessages); //update state and re-render
    
     function cleanup() {
      console.log("Component is being removed")
      offFunction();
     }
     return cleanup;
    })
  }, []);

Use the data to update the message and add the unregister function

Now update our state variable with what we have in firebase

start with empty array of messages

onValue() returns the function to turn itself off

return the function that will be run when component is removed from DOM

Firebase : Arrays

  • Firebase does not directly support arrays
     
  • This is because it needs to support concurrent access and indexes could change if multiple are manipulating items at same time.
     
  • Firebase treats all items as Objects, thus each value in the JSON tree has a unique key.
     
  • Firebase offers the push() method for adding values with an auto-generated key

Firebase : Using Push to add data to FB Array 

Produces the following structure in the firebase database:

  • Notice how each task is an object with a unique key
  • Instead of using an array index from 0 to length, you use the key as index

Ways to "Array-ify" the Firebase Objects 

Firebase supports "forEach()" to iterate through elements

To do more complex actions, (like map() or filter() )  you need a real array.

Call Object.keys() on the snapshot.val() to get an array of keys.

You can then iterate/map using bracket notation.

  • When iterating through array this way, you need to keep track of the key in the object as identifier to ref() and modify later

Get the task objects from snapshot

Call Object.keys() to get array of keys

now you have an array to do your normal stuff

...
import { getDatabase, ref, set as firebaseSet, onValue, push as FirebasePush} from 'firebase/database'; 
...
export default function ChatPage(props) {
...
  const db = getDatabase();
  const allMessagesRef = ref(db, "allMessages");
  
  useEffect(() => {
    
      const offFunction = onValue(allMessagesRef, function(snapshot) {
      const allMessagesObj = snapshot.val();
      console.log(allMessagesObj);
      const objKeys = Object.keys(allMessagesObj);
      
      const objArray = objKeys.map((keyString) => {
        allMessagesObj[keyString].key = keyString;
        return allMessagesObj[keyString];
      })
      
     setChatMessages(objArray); //update state and re-render
    
     function cleanup() {
      console.log("Component is being removed")
      offFunction();
     }
     return cleanup;
    })
  }, []);

pull together, messagearray, add key

add firebase push method

set State to the whole messageArray

point to allMessage ref now

get the object containing all message objects

create array of keys

map to array message objects

Add the firebase reference key to the object for later use

import React, { useState } from 'react';
import { getDatabase, ref, set as firebaseSet} from 'firebase/database'; 

export function ChatPane(props) {
...
}
function MessageItem(props) {
  const { userName, userImg, text, key, liked } = props.messageData;

  //state
  // const [isLiked, setIsLiked] = useState(false);

  const handleClick = (event) => {
    console.log("you liked " + userName + "'s post!");
    const db = getDatabase();
    const likeRef = ref(db, "allMessages/"+key+"/liked");
    // setIsLiked(!isLiked); //toggle
    firebaseSet(likeRef, true)
  }

  //RENDERING
  let heartColor = "grey";
  if (liked) {
    heartColor = "red";
  }

  return (
 ...
    <button className="btn like-button" onClick={handleClick}>
...
  )
}

Fix up the 'like' feature in MessgeItem

import firebase stuff

no longer using

set 'liked' value in firebase when clicked on

read in item key and liked value

Effect Hooks

An effect hook is used to specify "side effects" of Component rendering -- code you want to execute only once and not on each render!

//import the hooks used
import React, { useState, useEffect } from 'react';

function MyComponent(props) {
  const [stateData, setStateData] = useState([]);

  //specify the effect hook function
  useEffect(() => {
    //code to do only once here!
    //asynchronous methods (like fetch), etc
    fetch(dataUri) //e.g., send AJAX request 
    //...
    setStateData(data); //okay to call state setters here
    
  }, []) //array is the second arg to the `useEffect()` function
         //It lists which variables will "rerun" the hook if they 
         //change

  //...
}

Effect Hook Cleanup

In order to "clean up" any work done in an effect hook (e.g., disconnect listeners), have the callback return a "cleanup function" to execute. Not needed for fetch()

import React, { useState, useEffect } from 'react';

function MyComponent(props) {
  //specify the effect hook function
  useEffect(() => {

    //...do persistent work, set up subscriptions, etc

    //function to run when the Component is being removed
    function cleanup() {
      console.log("component being removed")
    }    
    return cleanup; //return function for React to call later    
  }, [])

  return ...
}

Action Items!

  • Review Everything

  • Problem Set 08 and 09 due Friday

  • Final Project due June 3. No extensions

Next time: Firebase Authentication

info340sp24-firebase-db

By Tim Carlson

info340sp24-firebase-db

  • 98