What we are looking for: Refactored Draft 1 into a React App
Converted the HTML/CSS from draft 1 into a published React app. Began to add interactive functionality.
import React, {useState} from 'react';
import { HeaderBar } from './HeaderBar.js';
...etc
function App(props) {
const [messageStateArray, setMessageStateArray] = useState(INITIAL_HISTORY);
const [currentUser, setCurrentUser] = useState(DEFAULT_USERS[1]) //initialize;
const addMessage = function(userObj, messageText, channel) {
const newMessage = {
"userId": userObj.userId,
... etc
}
const newArray = [...messageStateArray, newMessage];
setMessageStateArray(newArray); //update state & re-render
}
const changeUser = (newUserObj) => {setCurrentUser(newUserObj);}
return (
<div className="container-fluid d-flex flex-column">
<HeaderBar currentUser={currentUser} />
<ChatPage
currentUser={currentUser} messageArray={messageStateArray} addMessageFunction={addMessage} />
{/* <SignInPage currentUser={currentUser} changeUserFunction={changeUser} />
<Static.WelcomePage />
<Static.AboutPage />
<Static.ErrorPage /> */}
</div>
);
}
import React from 'react';
import _ from 'lodash';
import { ChannelList } from './ChannelList.js';
import { ChatPane } from './ChatPane.js';
export default function ChatPage(props) {
const {currentUser, messageArray, addMessageFunction} = props;
const channelNames = ["general", "channel-2", "birds", "dank-memes", "random"];
const currentChannel = "general";
//count how many messages are in each channel (using external library)
const channelCounts = _.countBy(messageArray, 'channel')
return (
<div className="row flex-grow-1">
<div className="col-3">
<ChannelList channelNames={channelNames} channelCounts={channelCounts} currentChannel={currentChannel} />
</div>
<div className="col d-flex flex-column">
<ChatPane
currentUser={currentUser}
messageArray={messageArray}
addMessageFunction={addMessageFunction}
/>
</div>
</div>
)
}
export default function SignInPage(props) {
const { currentUser, changeUserFunction} = props;
const handleClick = (event) => {
const whichUser = event.currentTarget.name //access button, not image
const selectedUserObj = DEFAULT_USERS.filter((userObj) => userObj.userId === whichUser)[0]
|| DEFAULT_USERS[0] //null user if not found
changeUserFunction(selectedUserObj) }
const userButtons = DEFAULT_USERS.map((userObj) => {
let classListString = "btn user-icon"
return (
<button className={classListString} key={userObj.userName}
name={userObj.userId} onClick={handleClick}>
<img src={userObj.userImg} alt={userObj.userName + " avatar"} />
</button>
) })
return (
<div className="card bg-light">
<div className="container card-body">
<p className="lead">Pick a user:</p>
<div>
{userButtons}
</div>
</div>
</div>
)
}
React components are structured to be self-contained, re-usable elements... so there are lots of pre-defined components online you can use!
In order to use a component in your app:
npm install lib-name
import { ComponentName } from 'lib-name'
<ComponentName />
import React from 'react';
import Dropdown from 'react-bootstrap/Dropdown';
import DEFAULT_USERS from '../data/users.json';
export default function SignInPage(props) {
const { currentUser, changeUserFunction} = props;
const handleClick = (event) => {
...etc }
const userButtons = DEFAULT_USERS.map((userObj) => {
...etc })
return (
<div className="card bg-light">
<div className="container card-body">
<p className="lead">Pick a user:</p>
<div>
<Dropdown>
<Dropdown.Toggle variant="light">
<img src={currentUser.userImg} alt={currentUser.userName + " avatar"} />
</Dropdown.Toggle>
<Dropdown.Menu>
{userButtons}
</Dropdown.Menu>
</Dropdown>
</div>
</div>
</div>
)
}
export default function App(props) {
...
return (
<div className="container-fluid d-flex flex-column">
<HeaderBar currentUser={currentUser} />
{/* <ChatPage currentUser={currentUser} /> */}
{/* <SignInPage currentUser={currentUser} loginCallback={loginUser} /> */}
<Static.WelcomePage />
<Static.AboutPage />
<Static.ErrorPage />
</div>
);
}
Render a different component depending on the URL.
"IF the current url MATCHES a route, render this Component!"
function App(props) {
//pick a component based on the URL
let componentToRender = null;
if(currentUrl === '/home'){ //pseudocode comparison with URL
componentToRender = <HomePage />;
}
else if(currentUrl === '/about'){
componentToRender = <AboutPage />;
}
//render that component
return <div>{componentToRender}</div>;
}
react-router
A library of React Components that can determine which other components to render based on the current URL.
We are using this as a library, not a framework.
//in App.js
import { Routes, Route } from 'react-router';
# Install library (on command line)
npm install react-router
The BrowserRouter component will keep track of the current URL in its state, and re-renders descendent components if that changes.
//main.js
import { BrowserRouter } from 'react-router'
import App from './components/App.js'
//render the App *inside* of the BrowserRouter
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
Add the BrowserRouter around our <App> in main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router';
//import CSS
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';
import App from './components/App';
const root = ReactDOM.createRoot(document.getElementById('root'));
// root.render(<App />);
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
import BrowserRouter
Wrap the <App> in the BrowserRouter Component
Pass elements in a Route Component to specify that they should only render when the URL matches a particular path. All routes go inside of a Routes element, which chooses which to "match" based on the URL
function App(props) {
return (
<Routes> {/* the collection of routes to match */}
{/* if currentUrlPath === "home" */}
<Route path="home" element={<HomePage />} />
{/* if currentUrlPath === "about" */}
<Route path="about" element={<AboutPage />} />
</Routes>
);
}
...
import { Routes, Route } from 'react-router';
export default function App(props) {
const [currentUser, setCurrentUser] = useState(DEFAULT_USERS[1])
const loginUser = (userObj) => { setCurrentUser(userObj); }
return (
<div className="container-fluid d-flex flex-column">
<HeaderBar currentUser={currentUser} />
<Routes>
<Route path="home" element={<Static.WelcomePage />} />
<Route path="chat" element={<ChatPage
currentUser={currentUser} />} />
<Route path="signin" element={<SignInPage
currentUser={currentUser} loginCallback={loginUser} />} />
<Route path="about" element={<Static.AboutPage />} />
<Route path="error" element={<Static.ErrorPage />} />
</Routes>
</div>
);
}
import Routes, Route
<Routes> wrap <Route>'s
<div className="container-fluid d-flex flex-column">
<HeaderBar currentUser={currentUser} />
<Routes>
<Route index element={<Static.WelcomePage />} />
<Route path="chat" element={<ChatPage
currentUser={currentUser} />} />
<Route path="signin" element={<SignInPage
currentUser={currentUser} loginCallback={loginUser} />} />
<Route path="about" element={<Static.AboutPage />} />
<Route path="*" element={<Static.ErrorPage />} />
</Routes>
</div>
"*" is a catchall
'index' route rather than " path='/' "
Use a Link element (in place of an <a>) to create state-based links between routes.
function Nav() {
return (
<nav>
<ul>
<li>
{/* replaces anchor element */}
<Link to="home">Home</Link>
</li>
<li>
<Link to="about">About</Link>
</li>
</ul>
</nav>
);
}
import { Link } from 'react-router';
export function HeaderBar(props) {
const currentUser = props.currentUser;
return (
<header className="text-light bg-primary px-1 d-flex justify-content-between">
<h1>React Chat</h1>
<ul className="nav nav-pills">
<li className="nav-item">
<Link className="nav-link" to="/">Home</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/chat">Chat</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/about">About</Link>
</li>
<li className="nav-item">
<Link className="nav-link" to="/signin">
<img src={currentUser.userImg} alt={currentUser.userName + " avatar"} />
</Link>
</li>
</ul>
</header>
)
import
change 'a' to 'Link'
change 'href' to 'to'
The Route path corresponds to a segment of a URL, with nested Route elements corresponding to each segment. Nested Routes will render in place of an Outlet component
function App(props) {
return (
<Routes>
<Route path="user" element={<UserLayout />} >
<Route path="profile" element={<UserProfile />} />
<Route path="favorites" element={<FavoriteItems />} />
</Route>
<Route path="items" element={ <ItemList />} />
</Routes>
);
}
function UserLayout(props) {
render (
<div className="user-layout">
<h1>User Page</h1>
<Outlet /> {/* will be replaced with <UserProfile/>, etc */}
</div>
)
}
A common use of nested routes is to make some routes protected, only showing content if e.g., the user is logged in.
import { Routes, Route, Outlet } from "react-router";
function RequireAuth(props) {
if(!userIsLoggedIn) {
return <p>Forbidden!</p>
}
else {
return <Outlet />
} }
function App(props) {
return (
<Routes>
{/* protected routes */}
<Route element={<RequireAuth />}>
<Route path="profile" element={<ProfilePage />} />
<Route path="secret" element={<SecretPage />} />
</Route>
{/* public routes */}
<Route path="signin" element={<SignInPage />} />
</Routes>
)
}
protected path parent
if user isn't logged in show 'forbidden', else show outlet (subroute)
...
import { Routes, Route, Outlet } from "react-router";
export default function App(props) {
... return (
<div className="container-fluid d-flex flex-column">
<HeaderBar currentUser={currentUser} />
<Routes>
<Route index element={<Static.WelcomePage />} />
<Route path="about" element={<Static.AboutPage />} />
<Route path="*" element={<Static.ErrorPage />} />
<Route path="signin" element={<SignInPage
currentUser={currentUser} loginCallback={loginUser} />} />
<Route element={<RequireAuth currentUser={currentUser} />}>
<Route path="chat" element={<ChatPage currentUser={currentUser} />} />
</Route>
</Routes>
</div>
);
}
function RequireAuth(props){
if(props.currentUser.userId === null) {
return <p>Forbidden!</p>
}
else {
return <Outlet />
}
}
Top 4 Route's are public
<Chat> is private - subroute of RequiredAuth
Is current user null? no access then
else show the Outlet (ChatPage)
http://www.domain.com/users
=> a list of users
HTTP requests are sent to a particular resource on the web, identified by its URI (think: web address).
Like postal addresses, URLs follow a particular format.
scheme (protocol)
how to access the information
domain
which web service has the resource
path
what resource to access
query
extra
parameters
(arguments) to the request
format:
?key=value&key=value&key=value
The web is based on the REST architecture. In this structure, each route (identifier, URI) should refer to a unique resource (set of information).
Think about what "information" should be found at each route. Come up with your routes first, and decide the components second!
function App(props) {
return (
<Routes>
{/* good routes */}
<Route path="/products" element={<AllProductsPage />} />
<Route path="/products/hat" element={<HatPage />} />
<Route path="/products/shoes" element={<ShoesPage />} />
<Route path="/account" element={<AccountPage />} />
{/* less good route -- an action, not a resource! */}
{/* (bad component definition as well) */}
<Route path="/purchase" element={<ProcessPurchase />} />
</Routes>
)
}
Many web services allow you to access their data through their web API.
E.g., GitHub! (https://docs.github.com/en/rest)
https://api.github.com/users/your-github-name/repos
# command-line downloading
curl https://api.github.com/users/info201/repos
# authenticate (to see private repos)
curl -u username https://api.github.com/repos/info201/repos
# include headers
curl -i https://api.github.com/users/info201/repos
Sometimes you have a multiple routes that show the same component, just for different data--where that data is specified by one of the segments!
function App(props) {
return (
<Routes>
<Route path="/products" element={<AllProductsPage />} />
{/* routes go to same "view", just different content based on url */}
<Route path="/products/hat" element={<ProductDetails item={"hat"} />} />
<Route path="/products/shoes" element={<ProductDetails item={"shoes"} />} />
</Routes>
)
}
A url parameter is a "variable" in the url's path. This is a shortcut for defining a large number of routes that point to (similar) resources.
/users/:username
/users/:username/repos
/orgs/:owner/:repo
/users/{username}/repos
A variable
Two variables
Alternate notation
Use :param syntax in the path to specify URL parameters. The useParams() hook lets you access the value of those parameters in the rendered element.
function App(props) {
return (
<Routes>
{/* if currentUrl == "posts/______" */}
{/* the string in the "blank" portion will be the `postId` param */}
<Route path="posts/:postId" element={<BlogPost />} />
</Routes>
);
}
import { useParams } from 'react-router-dom';
function BlogPost(props) {
const urlParams = useParams(); //access the URL params as an object
const postId = urlParams.postId; //can use destructuring instead
return (
{/* postId was the URL parameter from above! */}
<h1>You are looking at blog post {urlParams.postId}</h1>
)
}
export default function App(props) {
const [currentUser, setCurrentUser] = useState(DEFAULT_USERS[1])
const loginUser = (userObj) => {
setCurrentUser(userObj);
}
return (
<div className="container-fluid d-flex flex-column">
<HeaderBar currentUser={currentUser} />
<Routes>
<Route index element={<Static.WelcomePage />} />
<Route path="about" element={<Static.AboutPage />} />
<Route path="*" element={<Static.ErrorPage />} />
<Route path="signin" element={<SignInPage
currentUser={currentUser} loginCallback={loginUser} />} />
<Route element={<RequireAuth currentUser={currentUser} />}>
<Route path="chat/:channelName" element={<ChatPage currentUser={currentUser} />} />
<Route path="chat" element={<ChatPage currentUser={currentUser} />} />
</Route>
</Routes>
</div>
);
}
syntax to specify the url param
Note the added a 2nd 'chat' path for the case where there is no url param
...
import { useParams } from 'react-router-dom';
...
export default function ChatPage(props) {
...
const urlParamsObj = useParams();
console.log(urlParamsObj);
...
const currentChannel = urlParamsObj.channelName;
...
return (
<div className="row flex-grow-1">
<div className="col-3">
<ChannelList channelNames={channelNames} channelCounts={channelCounts}
currentChannel={currentChannel} />
</div>
<div className="col d-flex flex-column">
<ChatPane
currentUser={currentUser}
messageArray={messageArray}
addMessageFunction={addMessageFunction}
/>
</div>
</div>
)
}
set the channel from the url param
get the param object
import useParams hook
...
import {useParams} from 'react-router-dom';
export function ChatPane(props) {
const paramResult = useParams();
console.log("paramResult in ChatPane :", paramResult);
const currentChannel = paramResult.channelName || "general";
// const currentChannel = "general" //hard code for the moment
...
return (
<> {/* fake div */}
<div className="scrollable-pane pt-2 my-2">
{ messageArray.length === 0 &&
<p>No messages found</p>
}
{messageElemArray}
</div>
<ComposeForm
currentUser={currentUser}
currentChannel={currentChannel}
addMessageFunction={addMessageFunction} />
</>
)
}
set the channel from the url param
get the param object
import useParams hook
pass current channel url param as prop
import React from 'react';
import { Link } from 'react-router-dom';
export function ChannelList(props) {
const channels = props.channels;
const currentChannel = props.currentChannel;
const linkElemArray = channels.map((channelNameString) => {
let classList = "btn btn-sm btn-outline-light my-1";
if(channelNameString === currentChannel) {
classList = "btn btn-sm btn-warning"
}
const element = (
<div key={channelNameString}>
<Link className={classList} to={"/chat/" +channelNameString}>
{channelNameString}</Link>
</div>
)
return element;
})
return (
<nav className="text-light bg-secondary h-100 py-3 channel-nav px-2">
{linkElemArray}
</nav>
)
}
Fix the nav links to include the url param
GitHub pages is not able to cleanly handle client-side routing, so we'll use Firebase to host your projects instead!
Firebase is a web backend solution; it provides multiple features which you can access without need to "code" them yourselves.
next weeks
Use a combination of Firebase command line tools and Vite scripts to build and deploy your React application to Firebase hosting!
See Project Draft 2 instructions on Canvas for details.
Complete task list for Week 8 (items 1-5 !!)
Problem Set 08 due Wednesday (it is small)
Project Draft 2 due SUNDAY!!
Next time: React Review / Work Time
The web is based on the REST architecture. In this structure, each route (identifier, URI) should refer to a unique resource (set of information).
Think about what "information" should be found at each route. Come up with your routes first, and decide the components second!
function App(props) {
return (
<Routes>
{/* good routes */}
<Route path="/products" element={<AllProductsPage />} />
<Route path="/products/hat" element={<HatPage />} />
<Route path="/products/shoes" element={<ShoesPage />} />
<Route path="/account" element={<AccountPage />} />
{/* less good route -- an action, not a resource! */}
{/* (bad component definition as well) */}
<Route path="/purchase" element={<ProcessPurchase />} />
</Routes>
)
}
Sometimes you have a multiple routes that show the same component, just for different data--where that data is specified by one of the segments!
function App(props) {
return (
<Routes>
<Route path="/products" element={<AllProductsPage />} />
{/* routes go to same "view", just different content based on url */}
<Route path="/products/hat" element={<ProductDetails item={"hat"} />} />
<Route path="/products/shoes" element={<ProductDetails item={"shoes"} />} />
</Routes>
)
}