<info 340/>

React Router

Tim Carlson
Winter 2025

View of the Day

  • React Libraries (react-bootstrap)

  • Single Page Applications (lecture)

  • React Router (code demo)

  • URL Parameters (lecture + code demo)

  • Firebase Hosting (demo)

Project Draft 2

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.

  • ALL of content rendered: You should have Components that render DOM for every part of your page
  • Has Components w/ props and data: Organize your Components! Can hard-code sample data for now
  • Uses routes to show all page content (today!)
  • Has 1 feature implemented: Includes event handling and state manipulation (today). Can be the small feature!
  • Fixes issues from draft 1: You're revising the HTML/CSS, fix problems while you're at it!
  • Published to Firebase hosting: get that working this draft
    (see assignment for details; demo today!)

Our chat app so far...

Demo- Chat Page

Demo- SignIn Component

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>
  );
}

App Component start

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>
  )
}

Chat Page Component 

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>
  )
}

Signin Page Component 

React Libraries

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:

  1. Find it! (via npm, google, etc). Read the documentation
  2. Install it! npm install lib-name
  3. Import it! import { ComponentName } from 'lib-name'
    • (import structure may vary per library)
  4. Render it! <ComponentName />
    • Remember to pass any expected props!
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>
  )
}

Signin Page - React Bootstrap

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>
  );
}

3 other static Components

Single-Page Applications

Client-Side Routing

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.

Install Library

Import Components

//in App.js
import { Routes, Route } from 'react-router';
# Install library (on command line)
npm install react-router

BrowserRouter

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>
);

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

Routes

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>
  );
}

Routes/Route

...
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

Routes/Route

    <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='/' "

Links

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>
  );
}

Links

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'

Nesting Routes

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>
  )
}

Protected Routes

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)

Protected Routes Nexted (App changes)

...
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)

Uniform Resource Indicator

http://www.domain.com/users => a list of users

  • The address is the identifier, the list is the resource

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

URL Format

Naming Routes

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

Example URIs (API)

"Variable" 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>
  )
}

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.

  • URL parameters are different than query params

URL Parameters

/users/:username

/users/:username/repos

/orgs/:owner/:repo

/users/{username}/repos

A variable

Two variables

Alternate notation

URL Params

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>
  )
}

URL Params App Page

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

URL Params Chat Page

...
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

URL Params Chat Pane

...
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

URL Params ChannelNav Page

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

Hosting on Firebase

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.

  • Web Hosting
  • Databases
  • User Authentication

next weeks

React Deployment to Firebase

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.

Action Items!

  • 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

  • How would you like to use this time?

Naming Routes

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>
  )
}

"Variable" 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>
  )
}