Tim Carlson
I'm a lecturer at University of Washington
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/'); //go to chat after login
}
}
...
<Routes>
...
{/* protected routes */}
<Route element={<ProtectedPage currentUser={currentUser} />}>
<Route path="chat/:channelName" element={<ChatPage currentUser={currentUser} />} />
</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 outside of 'return' statement code
Navigate Component - used to change url path within a 'return' statement
Example 1
Example 2
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
import React from 'react';
import { ChannelList } from './ChannelList.jsx';
import { ChatPane } from './ChatPane.jsx';
const CHANNEL_NAMES = ["general", "social", "random", "birds", "dank-memes"];
export default function ChatPage(props) {
const {currentUser, messageArray, addMessageFunction} = props;
return (
<div className="row flex-grow-1">
<div className="col-3">
<ChannelList channelNames={CHANNEL_NAMES} />
</div>
<div className="col d-flex flex-column">
<ChatPane
currentUser={currentUser}
messageArray={messageArray}
addMessageFunction={addMessageFunction}
/>
</div>
</div>
)
}
import React, {useState} from 'react';
import Button from 'react-bootstrap/Button'
import { useParams } from 'react-router-dom';
import { ComposeForm } from './ComposeForm.jsx';
export function ChatPane(props) {
const { messageArray, addMessageFunction, currentUser} = props;
//from url parameters
const paramsObj = useParams();
const currentChannel = paramsObj.chanName || "general" //default
//data processes
const messagesToShow = messageArray
.filter((messageObj) => {
return messageObj.channel === currentChannel; //keep
})
.sort((m1, m2) => m2.timestamp - m1.timestamp); //reverse chron order
//content to display
const messageElemArray = messagesToShow.map((messageObj) => {
const messageElem = (
<MessageItem messageData={messageObj} key={messageObj.timestamp} />
);
return messageElem; //put it in the new array!
});
const handleTestClick = (event) => {
console.log("testing...");
}
///...more on next slide
just a test test function for showing firebase stuff off button click
using params for current channel
filter list (and sort) to current channel
transform to message items
return (
<> {/* fake div */}
<div className="scrollable-pane pt-2 my-2">
<Button className="justify-content-start" variant="warning" onClick={handleTestClick}> Test</Button>
<p></p>
{/* conditional rendering */}
{ messageElemArray.length === 0 &&
<p>No messages found</p>
}
{messageElemArray}
</div>
<ComposeForm
currentUser={currentUser}
currentChannel={currentChannel}
addMessageFunction={addMessageFunction} />
</>
)
}
///...more on next slide
Compose Form now is rendered in Chat Pane.
Conditional rendering: if there are no messages display that fact
Test button for demo'ing firebase at start of lecture
function MessageItem(props) {
const messageData = props.messageData;
const {userName, userImg, text, isLiked} = messageData;
const handleClick = function(event) {
console.log("you like me! you really like me!")
}
//decide what it looks like
let buttonColor = "grey";
if(isLiked) {
buttonColor = "red"; //filled in
}
return (
<div className="message d-flex mb-3">
<div className="me-2">
<img src={userImg} alt={userName+"'s avatar"}/>
</div>
<div className="flex-grow-1">
<p className="user-name">{userName}</p>
<p>{text}</p>
<button className="btn like-button" onClick={handleClick}>
<span className="material-icons" style={{ color: buttonColor }}>favorite_border</span>
</button>
</div>
</div>
)
}
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>
);
main.jsx
If you want to edit the date rules you can convert the date string to paste in: https://www.epochconverter.com/
Use "Timestamp in milliseconds"
database object in js file
database json object in firebase
...
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';
...
function App(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 function App(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 by using 'snapshot.val()'
export default function ChatPage(props) {
const [messageStateArray, setMessageStateArray] = useState([]);
...
const db = getDatabase();
const messageRef = ref(db, "message");
useEffect(() => {
const offFunction = onValue(messageRef, (snapshot) =>{
const newMessageObj = snapshot.val();
console.log(newMessageObj);
const updateMessageStateArray = [...messageStateArray, newMessageObj];
setMessageStateArray(updateMessageStateArray); //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 function App(props) {
...
const db = getDatabase();
const allMessagesRef = ref(db, "allMessages");
useEffect(() => {
const offFunction = onValue(allMessagesRef, function(snapshot) {
const allMessagesObj = snapshot.val();
const objKeys = Object.keys(allMessagesObj);
const objArray = objKeys.map((keyString) => {
allMessagesObj[keyString].key = keyString;
return allMessagesObj[keyString];
})
setChatMessages(objArray);
function cleanup() {
offFunction();
}
return cleanup;
})
}, []);
add firebase push method
set State to the whole messageArray
Point to 'allMessage' ref in firebase now
Add 'onValue' listener to reference which contains all message objects
Create array of keys
Map to array message objects
Add the firebase reference key to the object for later use. Adds the key to each object in your data structure in this new array.
I can use this for the 'liked' attribute on a message
...
import { getDatabase, ref, set as firebaseSet, onValue, push as FirebasePush} from 'firebase/database';
...
export function App(props) {
...
const db = getDatabase();
const allMessagesRef = ref(db, "allMessages");
...
const addMessage = (userObj, text, channel) => {
const newMessageObj = {
"userId": userObj.userId,
"userName": userObj.userName,
"userImg": userObj.userImg,
"text": text,
"timestamp": Date.now(),
"channel": channel
}
const db = getDatabase();
const allMessagesRef = ref(db,"allMessages");
FirebasePush(allMessagesRef, newMessageObj);
}
...
ref to the allMessages key
push new message to allMessages location in firebase
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;
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, !liked)
}
//RENDERING
let heartColor = "grey";
if (liked) {
heartColor = "red";
}
return (
...
<button className="btn like-button" onClick={handleClick}>
...
)
}
import firebase stuff
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 ...
}
Complete task list for Week 9 (all items
Review everything
Problem Set 09 due!
Next time: Firebase authentication and image storage
By Tim Carlson