For the Final Draft of the project, we're looking for it to be totally complete! See the Canvas page for full details.
Final Projects will be graded in two parts:
...
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
...
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
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
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 Messages
addMessage adds message by updating state
ChatForm - user enters content calls addMessage as prop
Firebase is a web backend solution; it provides multiple features which you can access without need to "code" them yourselves.
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>
);
index.js
database object in js file
database json object in firebase
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
You can modify an entry using the .set() method. It takes 2 arguments, the db reference and the new value
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 (
...
)
}
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 (
)
}
import libraries from fb
addMessage function in ChatPage
Write the newMessageObj to Firebase at the "message" key instead of updating the state variable
The set method returns promise because it is async
As a real-time DB, data can change anytime. Firebase provides event listener .onValue to allow apps to respond and update.
...
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 (
... )
}
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;
})
}, []);
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
Produces the following structure in the firebase database:
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.
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;
})
}, []);
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}>
...
)
}
import firebase stuff
no longer using
set 'liked' value in firebase when clicked on
read in item key and liked value
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
//...
}
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 ...
}
Review Everything
Problem Set 08 and 09 due Friday
Final Project due June 3. No extensions
Next time: Firebase Authentication